用于匹配最多 6 个单词分隔的 2 个数字(带有必需的小数点)的正则表达式?

Regular expression for matching 2 numbers (with required decimals) that are separated by at MOST 6 words?

提问人:Henley Wing Chiu 提问时间:9/24/2023 更新时间:9/25/2023 访问量:138

问:

我正在寻找构建一个匹配 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) 
正则表达式 Ruby

评论

0赞 jqurious 9/24/2023
您似乎在说数字的小数部分后面不能跟数字或空格,这就是它不匹配的原因: - 而模式的这一部分似乎试图定义“单词”?例如,更像是(\.[0-9]+))([^0-9\s] )(\.[0-9]+)) ([^0-9\s]+ ){1,6}
0赞 Wiktor Stribiżew 9/24/2023
不起作用?查看 regex101.com/r/RTLYaO/1\b(\d+\.\d+)((?:\s+[^\s\d]+)){0,6}\s+(-?\d+\.\d+)
0赞 bobble bubble 9/24/2023
\b(\d{1,3}\.\d+)(?: +[^\s\d]+){1,6} +(\d{1,3}\.\d+)
0赞 AdrianHHH 9/24/2023
也许数字匹配部分可能是 .这允许可选的前导零。数字的重要部分是 a 后跟最多 2 个 。最后是小数部分。0*[1-9][0-9]{0,2}\.[0-9]+[1-9][0-9]
0赞 Todd A. Jacobs 9/24/2023
如果你只想拉出小数,那么工作就好了。这里的挑战在于,您还需要满足许多其他验证和假设,特别是如果您要将其应用于比发布的有限示例更广泛的数据集。此外,在正则表达式中使用大小写独立性是没有意义的,因为罗马数字没有大写和小写版本。/\b(\d+\.\d+)\b/

答:

0赞 Cary Swoveland 9/24/2023 #1

我假设:

  • 十进制数是非负浮点数的字符串表示形式
  • 十进制数的前导数字只有在紧跟小数点时才能为零
  • “word”分隔符由一个或多个制表符或空格或其组合组成
  • 第一句中的“anything”是指包含除空格和数字以外的一个或多个字符的任何字符串

您可以匹配正则表达式

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

演示

例如

"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
/

评论

0赞 Cary Swoveland 9/24/2023
读者:我以为我可以将捕获组 2 定义为 ,其中执行定义捕获组 1 的令牌,但这不起作用。但是,它确实适用于 PCRE在此处搜索 “Subexpression Calls” 以获取 Ruby 的 文档。知道为什么不能像我预期的那样工作吗?(\g{1})\g{1}\g\g
0赞 Todd A. Jacobs 9/24/2023
\g需要命名捕获,因此需要使用捕获组,而不是数字或非捕获组。我还发现很难以这种方式指定重复模式,例如,虽然我认为有一种方法可以做到这一点——如果输入正确,上述方法甚至可能有效——但我没有在命名捕获上投入足够的资金作为解决今晚特定问题的解决方案。:)希望对您有所帮助!(?<foo>\p{Alpha}{3})\g<foo>(?<foo>\p{Alpha}{3})(?:\g<foo>){0,5}
0赞 Cary Swoveland 9/24/2023
@Todd,我给出的 Ruby 文档链接指出,“语法与上一个名为 name 的子表达式匹配,该子表达式可以再次是组名或编号。考虑和,这是我所期望的......\g<name>"abcdef".match /\A(?<foo>\p{Alpha}{3})\g<foo>\z/ #=> #<MatchData "abcdef" foo:"def">"abcdef".match /\A(\p{Alpha}{3})\g<1>\z/ #=> #<MatchData "abcdef" 1:"def">
0赞 Cary Swoveland 9/24/2023
...现在考虑 和 .我希望它们分别返回#<MatchData “abcdef” foo:“abc”、baz:“def”>' 和 。这就是我使用PCRE得到的。"abcdef".match /\A(?<foo>\p{Alpha}{3})(?<baz>\g<foo>)\z/ #=> #<MatchData "abcdef" foo:"def", baz:"def">"abcdef".match /\A(\p{Alpha}{3})(\g<1>)\z/ #=> #<MatchData "abcdef" 1:"def" 2:"def">#<MatchData "abcdef" 1:"abc" 2:"def">
0赞 Todd A. Jacobs 9/24/2023
你可能是对的。我只是说我也不能让它有一半的时间工作。:)
1赞 Todd A. Jacobs 9/24/2023 #2

分析

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

更面向对象的方法

考虑以下类,它通过将其他一些域逻辑和验证委托给专门的方法,使用更简单的正则表达式(参见 #dynamic_regex_pattern 方法,该方法在插值前只有大约 6 个原子)为您提供了正确的答案。它还使更改用于查找小数的主要正则表达式以及它们之间的最大距离更易于修改,因为它们是常量。另一方面,在不求助于子表达式的情况下测量它们之间适当距离的能力变得更加灵活,因为该类为每个特定的 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 对象提供合适的错误。任何其他数量的小数值都被视为错误,因为用于处理的业务逻辑未定义。
  2. 当两个小数点之间的距离大于允许的最大值时,提供错误消息。
  3. 可以使用 Hash 来识别有问题的 String 对象,只需检查键的值(如果存在)。:error
  4. 它区分不同类型的错误,您可以根据需要添加自己的处理程序。
  5. 它将接受任意数量的 String 对象作为参数或数组。
  6. 它将原始字符串存储为哈希键,并将十进制值存储在合理命名的子键中,以便于参考。
  7. 由于我的解决方案无论如何都必须计算小数点之间的距离,因此此解决方案将它们之间的距离存储为整数值作为奖励信息。

下面是上面示例 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 2.x、某些生命周期结束的 Ruby 或对核心类进行重大更改的未来 Ruby 版本,请根据需要随时进行调整。

还对问题域进行了一些假设。例如,最大距离 表示中间单词的数量,包括未分隔的标点符号,而不是包括开始和/或结束十进制值。代码足够灵活,可以根据需要进行更改。参见 #within_distance?了解实现细节。6

在最初的问题中,说你想“捕获 2 个数字”是模棱两可的,所以我选择将其解释为以一种明显且可检索的方式存储值。如果你的意思不同,可能会有其他更适合你的答案。

原始示例和发布的正则表达式都没有解决负值,例如 或(正如您有时在簿记中看到的那样)。连字符、UTF-8 减号等之间也存在差异。除非您对负值的实际字符表示形式做出更多假设,否则它可能比您想象的要复杂得多。因此,我没有解决那些超出范围的问题,尽管我显然选择解决我认为对这个特定问题很重要的其他问题。对于任何对负值有强烈感觉的人,即使它不是原始问题的一部分,请随时发布不同的答案。我相信有人会发现它有用!-14.02(5.30)

应用程序有几个地方的返回值本质上没有用处。例如,如果调用而不是:pp extractor.extract_results

extractor.extract_results
pp extractor.results

您将通过访问器获取 #extract_results 方法的返回值,而不是 @results Hash 的值。目前,两者是完全不同的,虽然前者会按照它所说的去做(它提取东西),但在完成所有检查和转换后,你真正想要的东西将在哈希中。

这些事情很容易修复,并且在生产应用程序中实现可能是一个好主意,在生产应用程序中,您希望能够轻松地对每个单独方法的返回值进行单元测试,但这超出了我在这里尝试演示的范围。