diff --git a/lib/validates_timeliness/formats.rb b/lib/validates_timeliness/formats.rb index 46a299d..2b46271 100644 --- a/lib/validates_timeliness/formats.rb +++ b/lib/validates_timeliness/formats.rb @@ -9,6 +9,9 @@ module ValidatesTimeliness mattr_accessor :valid_date_expressions mattr_accessor :valid_datetime_expressions + mattr_accessor :format_tokens + mattr_accessor :format_proc_args + # Format tokens: # # y = year @@ -17,9 +20,11 @@ module ValidatesTimeliness # h = hour # n = minute # s = second - # u = micro-second - # ampm = meridian (am or pm) with or without dots (eg, am, a.m, or a.m.) + # u = micro-seconds + # ampm = meridian (am or pm) with or without dots (e.g. am, a.m, or a.m.) # _ = optional space + # tz = Timezone abrreviation (e.g. UTC, GMT, PST, EST) + # zo = Timezone offset (e.g. +10:00, -08:00) # # All other characters are considered literal. You can embed regexp in the # format but no gurantees that it will remain intact. If you avoid the use @@ -32,6 +37,8 @@ module ValidatesTimeliness # yyyyy = exactly 4 digit year # mmm = month long name (e.g. 'Jul' or 'July') # + # u = Special case which matches 1 to 3 digits + # # Any other combination of repeating tokens will be swallowed up by the next # lowest length valid repeating token (e.g. yyy will be replaced with yy) @@ -57,7 +64,7 @@ module ValidatesTimeliness 'd/m/yyyy', 'd-m-yyyy', 'd.m.yyyy', - 'm/d/yy', + 'm/d/yy', 'd/m/yy', 'd-m-yy', 'd.m.yy', @@ -66,32 +73,72 @@ module ValidatesTimeliness ] @@valid_datetime_formats = [ + 'dd/mm/yyyy hh:nn:ss', + 'dd/mm/yyyy hh:nn', 'yyyy-mm-dd hh:nn:ss', 'yyyy-mm-dd hh:nn', - 'yyyy-mm-ddThh:nn:ss(?:Z|([-+]\d{2}:\d{2}))' + 'yyyy-mm-ddThh:nn:ss(?:Z|zo)' # iso 8601 ] - - def self.format_regexp_generator(string_format) - regexp = string_format.dup + + + # All tokens available for format construction. The token + # array is made of token regexp, validation regexp and key for + # format proc mapping if any. If the token needs no format + # proc arg then the validation regexp should not have a capturing + # group, as all captured groups are passed to the format proc. + @@format_tokens = [ + { 'mmm' => [ /m{3,}/, '(\w{3,9})', :month ] }, + { 'mm' => [ /m{2}/, '(\d{2})', :month ] }, + { 'm' => [ /(?:\A|[^ap])m{1}/, '(\d{1,2})', :month ] }, + { 'yyyy' => [ /y{4,}/, '(\d{4})', :year ] }, + { 'yy' => [ /y{2,}/, '(\d{2})', :year ] }, + { 'hh' => [ /h{2,}/, '(\d{2})', :hour ] }, + { 'h' => [ /h{1}/, '(\d{1,2})', :hour ] }, + { 'nn' => [ /n{2,}/, '(\d{2})', :min ] }, + { 'n' => [ /n{1}/, '(\d{1,2})', :min ] }, + { 'ss' => [ /s{2,}/, '(\d{2})', :sec ] }, + { 's' => [ /s{1}/, '(\d{1,2})', :sec ] }, + { 'u' => [ /u{1,}/, '(\d{1,3})', :usec ] }, + { 'dd' => [ /d{2,}/, '(\d{2})', :day ] }, + { 'd' => [ /(?:[^\\]|\A)d{1}/, '(\d{1,2})', :day ] }, + { 'ampm' => [ /ampm/, '((?:a|p)\.?m\.?)', :meridian ] }, + { 'zo' => [ /zo/, '(?:[-+]\d{2}:\d{2})'] }, + { 'tz' => [ /tz/, '(?:\[A-Z]{1,4})' ] }, + { '_' => [ /_/, '\s?' ] } + ] + + # Arguments whichs will be passed to the format proc if matched in the + # time string. The key must match the key from the format tokens. The array + # consists of the arry position of the arg, the arg name, and the code to + # place in the time array slot. The position can be nil which means the arh + # won't be placed in the array. + # + # The code can be used to run manipulations + # of the arg value if required, otherwise should just be the arg name. + # + @@format_proc_args = { + :year => [0, 'y', 'unambiguous_year(y)'], + :month => [1, 'm', 'm'], + :day => [2, 'd', 'd'], + :hour => [3, 'h', 'full_hour(h,md)'], + :min => [4, 'n', 'n'], + :sec => [5, 's', 's'], + :usec => [6, 'u', 'u'], + :meridian => [nil, 'md', nil] + } + + # Compile formats into validation regexps and format procs + def self.format_expression_generator(string_format) + regexp = string_format.dup order = {} ord = lambda {|k| order[k] = $~.begin(0) } - regexp.gsub!(/([\.\/])/, '\\1') - regexp.gsub!(/m{3,}/, '(\w{3,9})') && ord.call(:month) - regexp.gsub!(/m{2}/, '(\d{2})') && ord.call(:month) - regexp.gsub!(/(?:\A|[^ap])m{1}/, '(\d{1,2})') && ord.call(:month) - regexp.gsub!(/y{4,}/, '(\d{4})') && ord.call(:year) - regexp.gsub!(/y{2,}/, '(\d{2})') && ord.call(:year) - regexp.gsub!(/h{2,}/, '(\d{2})') && ord.call(:hour) - regexp.gsub!(/h{1}/, '(\d{1,2})') && ord.call(:hour) - regexp.gsub!(/n{2,}/, '(\d{2})') && ord.call(:min) - regexp.gsub!(/n{1}/, '(\d{1,2})') && ord.call(:min) - regexp.gsub!(/s{2,}/, '(\d{2})') && ord.call(:sec) - regexp.gsub!(/s{1}/, '(\d{1,2})') && ord.call(:sec) - regexp.gsub!(/u{1,}/, '(\d{1,3})') && ord.call(:usec) - regexp.gsub!(/d{2,}/, '(\d{2})') && ord.call(:day) - regexp.gsub!(/(?:[^\\]|\A)d{1}/, '(\d{1,2})') && ord.call(:day) - regexp.gsub!(/ampm/, '((?:a|p)\.?m\.?)') && ord.call(:meridian) - regexp.gsub!(/_/, '\s?') + regexp.gsub!(/([\.\/])/, '\\1') + + format_tokens.each do |token| + token_regexp, regexp_str, arg_key = *token.values.first + regexp.gsub!(token_regexp, regexp_str) && !arg_key.nil? && ord.call(arg_key) + end + format_regexp = Regexp.new(regexp) format_proc = format_proc(order) return format_regexp, format_proc @@ -101,7 +148,9 @@ module ValidatesTimeliness end # Generates a proc which when executed maps the regexp capture groups to a - # time array based on the order of the capture groups. + # proc argument based on order captured. A time array is built using the proc + # argument in the position indicated by the first element of the proc arg + # array. # # Examples: # @@ -109,16 +158,7 @@ module ValidatesTimeliness # 'dd/mm/yyyy h:nn_ampm' => lambda {|d,m,y,h,n,md| md||=0; [unambiguous_year(y),m,d,full_hour(h,md),n,nil,nil] } # def self.format_proc(order) - arg_map = { - :year => [0, 'y', 'unambiguous_year(y)'], - :month => [1, 'm', 'm'], - :day => [2, 'd', 'd'], - :hour => [3, 'h', 'full_hour(h,md)'], - :min => [4, 'n', 'n'], - :sec => [5, 's', 's'], - :usec => [6, 'u', 'u'], - :meridian => [nil, 'md', nil] - } + arg_map = format_proc_args args = order.invert.sort.map {|p| arg_map[p[1]][1] } arr = [nil] * 7 order.keys.each {|k| i = arg_map[k][0]; arr[i] = arg_map[k][2] unless i.nil? } @@ -128,13 +168,13 @@ module ValidatesTimeliness def self.compile_formats(formats) formats.collect do |format| - regexp, format_proc = format_regexp_generator(format) + regexp, format_proc = format_expression_generator(format) end end def self.compile_format_expressions - @@valid_time_expressions = compile_formats(@@valid_time_formats) - @@valid_date_expressions = compile_formats(@@valid_date_formats) + @@valid_time_expressions = compile_formats(@@valid_time_formats) + @@valid_date_expressions = compile_formats(@@valid_date_formats) @@valid_datetime_expressions = compile_formats(@@valid_datetime_formats) end