ruby 正则表达式匹配2个数字(需要小数),分隔最多6个字?

fafcakar  于 12个月前  发布在  Ruby
关注(0)|答案(2)|浏览(97)

我正在寻找构建一个正则表达式,匹配2个十进制数(必须包含小数点),最多由6个单词分隔(在这种情况下,单词意味着任何有空格的东西)。数字也必须< 1000,我想捕捉2个数字
这是我在正则表达式上的尝试,但它根本不匹配。

regex =  /([0-9]{1,3}(\.[0-9]+))([^0-9\s] ){1,6}([0-9]{1,3}(\.[0-9]+))\b/i

范例:

str = 'Min Hourly Rate 33.33 Max Hourly Range: 45.55'
regex.match(str) #returns nil

此外,本发明还提供了一种方法,

str = 'Min Hourly Rate 33.33 Max Hourly Range: 45.55 Max Hourly Range: 50.00'
regex.match(str) #should capture 'Min Hourly Rate 33.33 Max Hourly Range: 45.55' (the first match found)
ugmeyewa

ugmeyewa1#

分析

虽然你可以用一个Regexp来实现,但很可能是使用子表达式调用,这将过于复杂,难以调试。您还需要考虑许多其他边缘情况和条件,包括多价验证。与其试图在一个正则表达式中完成所有这些,不如将其划分为更小、更可测试的步骤。虽然这会使代码更长并且(可能)在视觉上不那么优雅,但结果是可以更容易地修改,扩展和测试。

更加面向对象的方法

考虑下面的类,它通过将一些其他域逻辑和验证委托给专门的方法,用一个更简单的正则表达式(请参阅#dynamic_regex_pattern方法,在插值之前只有大约六个原子)为您提供正确的答案。它还使更改用于查找小数的主正则表达式以及它们之间的最大距离变得更容易,因为它们是常量。另一方面,测量它们之间的适当距离的能力变得更加灵活,而无需求助于子表达式,因为类为每个特定的String构建了一个动态模式。

# Tested on mainline Ruby 3.2.2. Your
# mileage may vary with other versions.
class ExtractDecimalsFromStrings
  DECIMALS  = /\b(\d+\.\d+)\b/
  MAX_SEP   = 6
  SEP_WORDS = '#{first}\b.*?#{second}\b'

  attr_accessor :str_arr
  attr_reader :results

  def initialize *str
    @results = {}
    @str_arr = str.flatten
  end

  def extract_results
    @str_arr.each do |str|
      decimals = str.scan(DECIMALS).flatten.slice 0, 2
      decimal_error(str) && next unless valid? decimals
      populate_results_from str, decimals
    end
  end

  private

  def decimal_error str
    @results[str] = { error: "not enough decimals to parse" }
  end

  def distance_error str
    @results[str] = { error: "distance exceeds #{MAX_SEP}" }
  end

  def dynamic_regex_pattern decimals
    /#{Regexp.escape decimals.first}\b.*?#{Regexp.escape decimals.last}\b/
  end

  def populate_results_from str, decimals
    pat = dynamic_regex_pattern decimals
    distance = str.match(pat) { within_distance? $& }
    distance ? @results[str]={ first_decimal: decimals[0], second_decimal:
                               decimals[1], distance: distance } :
               distance_error(str)
  end

  def valid? decimals
    decimals.count == 2 && decimals.all? { _1.to_f.ceil.between? 0, 999 }
  end

  def within_distance? match
    words = [match&.to_s.split].flatten.compact
    words.shift && words.pop if words.count >= 2
    words.any? && words.count <= MAX_SEP && words.count
  end
end

example_strings = [
  "Min Hourly Rate 33.33 Max Hourly Range: 45.55",
  "Min Hourly Rate 33.33 Max Hourly Range: 45.55 Max Hourly Range: 50.00",
  "No Rates Specified",
  "33.33 is too far away from as defined by MAX_SEP: 65.00"
]

extractor = ExtractDecimalsFromStrings.new example_strings
extractor.extract_results
pp extractor.results

其他兴趣点

这不仅会给予你想要的基本输出,例如。两个十进制数在给定的距离内,但它也会:
1.为任何不包含正好两个十进制值的String对象提供适当的错误。任何其他小数位数的值都被视为错误,因为处理它的业务逻辑未定义。
1.当两个小数点之间的距离大于允许的最大值时,提供错误消息。
1.您可以使用Hash来识别有问题的String对象,只需检查:error键的值(如果存在)。
1.它区分不同类型的错误,您可以根据需要添加自己的处理程序。
1.它将接受任意数量的String对象作为参数或作为Array。
1.它将原始String存储为Hash键,并将十进制值存储在合理命名的子键中,以便于引用。
1.由于我的解决方案无论如何都要计算小数之间的距离,所以这个解决方案将它们之间的距离存储为一个值作为奖励信息。
下面是上面的String数组示例的输出:

{"Min Hourly Rate 33.33 Max Hourly Range: 45.55"=>
  {:first_decimal=>"33.33", :second_decimal=>"45.55", :distance=>3},
 "Min Hourly Rate 33.33 Max Hourly Range: 45.55 Max Hourly Range: 50.00"=>
  {:first_decimal=>"33.33", :second_decimal=>"45.55", :distance=>3},
 "No Rates Specified"=>{:error=>"not enough decimals to parse"},
 "33.33 is too far away from as defined by MAX_SEP: 65.00"=>{:error=>"distance exceeds 6"}}

注意事项

首先,这是在Ruby 3.2.2上完成的。输入和输出都经过了合理的严格测试,但在可预见的未来,没有(或将没有)努力使其与其他Ruby兼容。如果它不能在Ruby 2.x、某些即将结束的Ruby或对核心类进行了重大更改的Ruby未来版本上工作,请随时根据需要调整它。
对问题域也做了一些假设。例如,6的最大距离表示中间单词的数量,包括未分隔的标点符号,而不是包括开始和/或结束的十进制值。代码足够灵活,可以根据您的需要进行更改。#within_distance?以了解实施细节。
在最初的问题中,说你想“捕获2个数字”是模棱两可的,所以我选择将其解释为以一种明显且可检索的方式存储值。如果你的意思不同,可能会有其他更适合你的答案。
无论是最初的例子还是发布的正则表达式都没有处理负值,比如-14.02或(你有时会在簿记中看到)(5.30)。连字符、UTF-8减号等之间也存在差异。除非您对负值的实际字符表示做出更多的假设,否则它可能比您想象的更复杂。所以,我没有解决那些超出范围的问题,尽管我显然选择解决我认为对这个特定问题很重要的其他问题。对于那些对消极价值观有强烈感受的人,即使这不是原始问题的一部分,请随时发布不同的答案。我相信有人会发现它很有用!
这个应用程序有几个地方的返回值本质上并不有用。例如,如果调用pp extractor.extract_results而不是:

extractor.extract_results
pp extractor.results

你将通过访问器获得#extract_results方法的返回值,而不是 @results Hash的值。目前,这两个是完全不同的,虽然前者做它所说的(它提取的东西),你实际上 * 想要 * 将在所有检查和转换完成后的哈希。
这类事情很容易修复,并且可能是在生产应用程序中实现的好主意,您希望能够轻松地对每个单独方法的返回值进行单元测试,但这超出了我在这里试图演示的范围。

yruzcnhs

yruzcnhs2#

我假设:

  • 十进制数是非负浮点数的字符串表示形式
  • 一个十进制数的前导位只能是零,当它后面紧跟着小数点时
  • “单词”分隔符由一个或多个制表符或空格或其组合组成
  • 第一句中的“anything”表示任何包含一个或多个除空格和数字以外的字符的字符串

可以匹配正则表达式

rgx = /((?<!\d)(?:\d|[1-9]\d{1,2})\.\d+)(?:[\t ]+[^\s\d]+){0,6}[\t ]+((?<!\d)(?:\d|[1-9]\d{1,2})\.\d+)/

Demo
比如说,

"1.1 abc def 2.2".match(rgx)
  #=> #<MatchData "1.1 abc def 2.2" 1:"1.1" 2:"2.2">

正则表达式可以分解如下。

/
(                 # begin capture group 1
  (?<!\d)         # negative lookbehind asserts preceding char is not a digit
  (?:             # begin non-capture group
    \d            # match digit
    |             # or
    [1-9]         # match a digit other than zero
    \d{1,2}       # match 1 or 2 digits
  )               # end non-capture group    
  \.              # match period
  \d+             # match one or more digits
)                 # end capture group 1
(?:               # begin non-capture group
  [\t ]+          # match one or more tabs or spaces
  [^\s\d]+        # match one or more chars other than whitespaces or digits
)                 # end non-capture group
{0,6}             # execute non-capture group 0-6 times
[\t ]+            # match one or more tabs or spaces
(                 # begin capture group 2
  ...             # same tokens as those defining capture group 1
)                 # end capture group 2
/

相关问题