如何在单个字段上应用多个正则表达式

时间:2016-12-07 17:34:36

标签: ruby-on-rails ruby regex

目前我有一个美国邮政编码的正则表达式:

validates :zip,
          presence: true, 
          format: { with: /\A\d{5}(-\d{4})?\z/ }

我想在同一个邮政编码上对其他国家/地区使用不同的正则表达式,因此应根据国家/地区使用正则表达式:

  • 澳大利亚4位数
  • 加拿大6位数的字母数字
  • 英国6-7位数的字母数字

有人可以建议我如何满足我的要求?

3 个答案:

答案 0 :(得分:3)

你可以给一个返回Regexp的lambda作为格式验证器(see :with)的:with选项,这样可以保持良好和干净:

ZIP_COUNTRY_FORMATS = {
  'US' => /\A\d{5}(-\d{4})?\z/,
  'Australia' => /\A\d{4}\z/,
  # ...
}

validates :zip, presence: true,
  format: { with: ->(record){ ZIP_COUNTRY_FORMATS.fetch(record.country) } }

请注意,使用Hash#fetch代替Hash#[],这样如果给出了不存在的country,它将引发KeyError,就像进行健全性检查一样。或者,您可以返回与任何内容匹配的默认Regexp:

ZIP_COUNTRY_FORMATS.fetch(record.country, //)

......或者什么都没有:

ZIP_COUNTRY_FORMATS.fetch(record.country, /.\A/)

...取决于您想要的行为。

答案 1 :(得分:2)

您可能希望编写一种方法来帮助您:

  validates :zip, presence: true, with: :zip_validator

  def zip_validator
    case country
    when 'AU'
      # some regex or fail
    when 'CA'
      # some other regex or fail
    when 'UK'
      # some other regex or fail
    else
      # should this fail?
    end
  end

答案 2 :(得分:1)

假设我们以散列形式给出每个国家的有效邮政编码示例,如下所示。

example_pcs = {
  US:  ["",   "98230", "98230-1346"],
  CAN: ["*",  "V8V 3A2"],
  OZ:  ["!*", "NSW 1130", "ACT 0255", "VIC 3794", "QLD 4000", "SA 5664",
              "WA 6500", "TAS 7430", "NT 0874"]
}

其中每个数组的第一个元素是一串代码,稍后将对此进行解释。

我们可以根据这些信息为每个国家构建一个正则表达式。 (这些信息无疑会在实际应用中有所不同,但我只是提出了一般性的想法。)对于每个国家,我们为每个示例邮政编码构建一个正则表达式,部分使用上述代码。然后,我们将这些正则表达式联合起来,为该国家获得单一的正则表达式。这是构建示例邮政编码的正则表达式的一种方式。

def make_regex(str, codes='')
  rstr = str.each_char.chunk do |c|
    case c
    when /\d/          then :DIGIT
    when /[[:alpha:]]/ then :ALPHA
    when /\s/          then :WHITE
    else                    :OTHER
    end
  end.
  map do |type, arr|
    case type
    when :ALPHA
      if codes.include?('!')
        arr
      elsif arr.size == 1
        "[[:alpha:]]"
      else "[[:alpha:]]\{#{arr.size}\}"
      end
    when :DIGIT
      (arr.size == 1) ? "\\d" : "\\d\{#{arr.size}\}"
    when :WHITE
      case codes
      when /\*/ then "\\s*"
      when /\+/ then "\\s+"
      else (arr.size == 1) ? "\\s" : "\\s\{#{arr.size}\}"
      end
    when :OTHER
      arr
    end
  end.
  join
  Regexp.new("\\A" << rstr << "\\z")
end

我已经使正则表达式对字母不区分大小写,但这当然可以改变。此外,对于某些国家/地区,可能必须手动调整所生成的正则表达式和/或可能需要对邮政编码字符串进行一些预处理或后处理。例如,某些组合可能具有正确的格式,但仍然不是有效的邮政编码。例如,在澳大利亚,每个地区代码后面的四位数字必须落在因地区而异的指定范围内。

以下是一些例子。

make_regex("12345")
  #=> /\A\d{5}\z/
make_regex("12345-1234")
  #=> /\A\d{5}-\d{4}\z/
Regexp.union(make_regex("12345"), make_regex("12345-1234"))
  #=> /(?-mix:\A\d{5}\z)|(?-mix:\A\d{5}-\d{4}\z)/

make_regex("V8V 3A2", "*")
  #=> /\A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z/ 

make_regex("NSW 1130", "!*")
  # => /\ANSW\s*\d{4}\z/

然后,对于每个国家/地区,我们为每个示例邮政编码采用正则表达式的并集,将这些结果保存为哈希,其键是国家/地区代码。

h = example_pcs.each_with_object({}) { |(country, (codes, *examples)), h|
  h[country] = Regexp.union(examples.map { |s| make_regex(s, codes) }.uniq) }
  #=> {:US=>/(?-mix:\A\d{5}\z)|(?-mix:\A\d{5}-\d{4}\z)/,
  #    :CAN=>/\A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z/,
  #    :OZ=>/(?-mix:\ANSW\s*\d{4}\z)|(?-mix:\AACT\s*\d{4}\z)|(?-mix:\AVIC\s*\d{4}\z)|(?-mix:\AQLD\s*\d{4}\z)|(?-mix:\ASA\s*\d{4}\z)|(?-mix:\AWA\s*\d{4}\z)|(?-mix:\ATAS\s*\d{4}\z)|(?-mix:\ANT\s*\d{4}\z)/} 

"12345"      =~ h[:US]
  #=> 0
"12345-1234" =~ h[:US]
  #=> 0
"1234"       =~ h[:US]
  #=> nil
"12345 1234" =~ h[:US]
  #=> nil

"V8V 3A2"    =~ h[:CAN]
  #=> 0
"V8V    3A2" =~ h[:CAN]
  #=> 0
"V8v3a2"     =~ h[:CAN]
  #=> 0
"3A2 V8V"    =~ h[:CAN]
  #=> nil

"NSW 1132"   =~ h[:OZ]
   #=> 0
"NSW   1132" =~ h[:OZ]
   #=> 0
"NSW1132"    =~ h[:OZ]
   #=> 0
"NSW113"     =~ h[:OZ]
   #=> nil
"QLD"        =~ h[:OZ]
   #=> nil
"CAT 1132"   =~ h[:OZ]
   #=> nil

make_regex

执行的步骤
str = "V8V 3A2"
codes = "*+"

如下。

e = str.each_char.chunk do |c|
      case c
      when /\d/          then :DIGIT
      when /[[:alpha:]]/ then :ALPHA
      when /\s/          then :WHITE
      else                    :OTHER
      end
end
    #=> #<Enumerator: #<Enumerator::Generator:0x007f9ff201a330>:each>

我们可以看到这个枚举器通过将它转换为数组而生成的值。

e.to_a
  #=> [[:ALPHA, ["V"]], [:DIGIT, ["8"]], [:ALPHA, ["V"]], [:WHITE, [" "]],
  #    [:DIGIT, ["3"]], [:ALPHA, ["A"]], [:DIGIT, ["2"]]] 

继续,

a = e.map do |type, arr|
    case type
    when :ALPHA
      if codes.include?('!')
        arr
      elsif arr.size == 1
        "[[:alpha:]]"
      else "[[:alpha:]]\{#{arr.size}\}"
      end
    when :DIGIT
      (arr.size == 1) ? "\\d" : "\\d\{#{arr.size}\}"
    when :WHITE
      case codes
      when /\*/ then "\\s*"
      when /\+/ then "\\s+"
      else (arr.size == 1) ? "\\s" : "\\s\{#{arr.size}\}"
      end
    when :OTHER
      arr
    end
  end
    #=> ["[[:alpha:]]", "\\d", "[[:alpha:]]", "\\s*", "\\d", "[[:alpha:]]", "\\d"]
rstr = a.join
  #=> "[[:alpha:]]\\d[[:alpha:]]\\s*\\d[[:alpha:]]\\d" 
t = "\\A" << rstr << "\\z"
  #=> "\\A[[:alpha:]]\\d[[:alpha:]]\\s*\\d[[:alpha:]]\\d\\z" 
puts t
  #=> \A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z
Regexp.new(t)
  #=> /\A[[:alpha:]]\d[[:alpha:]]\s*\d[[:alpha:]]\d\z/