moved tokens and proc arg map into class arrays

This commit is contained in:
Adam Meehan 2008-07-18 11:27:32 +10:00
parent c9a3075c35
commit e18d0ee9f4

View File

@ -9,6 +9,9 @@ module ValidatesTimeliness
mattr_accessor :valid_date_expressions mattr_accessor :valid_date_expressions
mattr_accessor :valid_datetime_expressions mattr_accessor :valid_datetime_expressions
mattr_accessor :format_tokens
mattr_accessor :format_proc_args
# Format tokens: # Format tokens:
# #
# y = year # y = year
@ -17,9 +20,11 @@ module ValidatesTimeliness
# h = hour # h = hour
# n = minute # n = minute
# s = second # s = second
# u = micro-second # u = micro-seconds
# ampm = meridian (am or pm) with or without dots (eg, am, a.m, or a.m.) # ampm = meridian (am or pm) with or without dots (e.g. am, a.m, or a.m.)
# _ = optional space # _ = 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 # 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 # 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 # yyyyy = exactly 4 digit year
# mmm = month long name (e.g. 'Jul' or 'July') # 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 # 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) # 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', 'd-m-yyyy',
'd.m.yyyy', 'd.m.yyyy',
'm/d/yy', 'm/d/yy',
'd/m/yy', 'd/m/yy',
'd-m-yy', 'd-m-yy',
'd.m.yy', 'd.m.yy',
@ -66,32 +73,72 @@ module ValidatesTimeliness
] ]
@@valid_datetime_formats = [ @@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:ss',
'yyyy-mm-dd hh:nn', '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 = {} order = {}
ord = lambda {|k| order[k] = $~.begin(0) } ord = lambda {|k| order[k] = $~.begin(0) }
regexp.gsub!(/([\.\/])/, '\\1') regexp.gsub!(/([\.\/])/, '\\1')
regexp.gsub!(/m{3,}/, '(\w{3,9})') && ord.call(:month)
regexp.gsub!(/m{2}/, '(\d{2})') && ord.call(:month) format_tokens.each do |token|
regexp.gsub!(/(?:\A|[^ap])m{1}/, '(\d{1,2})') && ord.call(:month) token_regexp, regexp_str, arg_key = *token.values.first
regexp.gsub!(/y{4,}/, '(\d{4})') && ord.call(:year) regexp.gsub!(token_regexp, regexp_str) && !arg_key.nil? && ord.call(arg_key)
regexp.gsub!(/y{2,}/, '(\d{2})') && ord.call(:year) end
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?')
format_regexp = Regexp.new(regexp) format_regexp = Regexp.new(regexp)
format_proc = format_proc(order) format_proc = format_proc(order)
return format_regexp, format_proc return format_regexp, format_proc
@ -101,7 +148,9 @@ module ValidatesTimeliness
end end
# Generates a proc which when executed maps the regexp capture groups to a # 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: # 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] } # '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) def self.format_proc(order)
arg_map = { arg_map = 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]
}
args = order.invert.sort.map {|p| arg_map[p[1]][1] } args = order.invert.sort.map {|p| arg_map[p[1]][1] }
arr = [nil] * 7 arr = [nil] * 7
order.keys.each {|k| i = arg_map[k][0]; arr[i] = arg_map[k][2] unless i.nil? } 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) def self.compile_formats(formats)
formats.collect do |format| formats.collect do |format|
regexp, format_proc = format_regexp_generator(format) regexp, format_proc = format_expression_generator(format)
end end
end end
def self.compile_format_expressions def self.compile_format_expressions
@@valid_time_expressions = compile_formats(@@valid_time_formats) @@valid_time_expressions = compile_formats(@@valid_time_formats)
@@valid_date_expressions = compile_formats(@@valid_date_formats) @@valid_date_expressions = compile_formats(@@valid_date_formats)
@@valid_datetime_expressions = compile_formats(@@valid_datetime_formats) @@valid_datetime_expressions = compile_formats(@@valid_datetime_formats)
end end