Techioz Blog

関数を備えた Ruby イテレータ。リスト全体を反復せずに関数から最初の値を返します

概要

配列: arr=[x1, x2, x3…] と、関数が真実であることを示す arr の最初の x に基づいて値を返す関数があります。

基本的に:

# my_x is the return of func() 
# with the first x in arr that func(x) is true
# and the entire arr array is not processed.

my_x=arr.ruby_magic{|x| func(x) } 

my_x should be equal to first true value return of func(x)

arr の各 X が正規表現パターンであると仮定します。すべての正規表現を実行することなく、最初の一致からキャプチャ グループを返したいと考えています。

Python では、next を使用してジェネレーターを作成します。真実が返されるまで各述語を実行し、その値を m に渡します。真実が返されない場合は、デフォルトとして None が使用されますが、そのデフォルトは何でも構いません。

import re 

patterns=[r"no match", r": (Value.*?pref)", r": (Value.*)", r"etc..."]

s=""" 
This is the input txt
This is a match if the other is not found: Value 1

This is the match I am looking for first: Value 1 pref

Last line.
"""

val_I_want=next(
        (m.group(1) for p in patterns 
            if (m:=re.search(rf'{p}', s))), None)

Ruby では同等のものを見つけられませんでした。

明示的なループを実行できます。

# s in the same multiline string as above...

patterns=[/no match/, /: (Value.*?pref)/, /: (Value.*)/,/etc.../]

val_I_want=nil 
patterns.each{|p| 
    m=p.match(s)
    if m then
        val_I_want=m[1]
        break 
    end     
}
# val_I_want is either nil or 
# the first match capture group that is true

これが私が望む機能ですが、Python ジェネレーターと比較すると少し冗長に思えます。

最初の値を述語として grep を試してみました。しかし、ここでの問題は、結果配列全体が次に使用される前に生成されることです。

patterns.grep(proc {|p| p.match(s)}) {|m| m.match(s)[1]}.to_enum
# can then use .next on that.
#BUT it runs though the entire array when all I want is the first

#<Enumerator: ["Value 1 pref", "Value 1"]:each>

find を試みましたが、キャプチャ グループではなく、true である最初のパターンが返されました。

> e=patterns.find{|p| p.match(s) }
=> /: (Value.*?pref)/

# Now I would have to rerun match with the pattern found to get the text

アイデアは?

役立つアイデアをありがとうございました。 Ruby キットバッグの中でいくつかの新しいことを学びました。

いくつか調べて試してみた結果、私にとって最良の方法は、Dogbert の Lazy.filter_map と Stefan の提案である s[regex, 1] を組み合わせることだと思います。

val_I_want=patterns.lazy.filter_map { |p| s[p, 1] }.first

興味深いことに、構文 s[p, 1] は、[] 演算子内の括弧のない動的正規表現 (Regexp.new “#{p.to_s}(.*)“) をサポートしていないため、魅力が損なわれます。

私は最終的に使用しました:

patterns.lazy.filter_map { |p| card.match("#{p}(.*)")&.[](1) }.first

しかし、これも機能します:

patterns.find{ |p| m = card.match("#{p}(.*)") and break m[1] }

より一般的なケースでは、次のようにすることができます。

def func(x)
  # silly function for show
  x*x
end     

arr=[1,3,5,6,7,8,9]

p arr.lazy.filter_map { |x| (fx=func(x))>30 ? [x,fx] : nil }.first
# [6, 36]

そして、engineersmnky による私の .find の試みの修正に対する非常に名誉ある言及です。

val_I_want = patterns.find {|p| m = p.match(s) and break m[1] }
   

解決策

.lazy.filter_map { .. }.first を使用できます。これにより、最初の真の値が見つかった後は要素のブロックは実行されません。

irb> [1, 2, 3, 4, 5].lazy.filter_map { |x| p x; x > 3 ? x * 2 : nil }.first
1
2
3
4
=> 8

これにより、3 より大きい最初の x に対して x * 2 が返されます。私は p x を追加しました。このコードがリストの 5 番目の要素を処理しないことを示します。

正規表現の例:

irb> regexes = [/(1)/, /(2)/, /(3)/]
=> [/(1)/, /(2)/, /(3)/]
irb> regexes.lazy.filter_map { |regex| p regex; regex.match("2")&.[](1) }.first
/(1)/
/(2)/
=> "2"

以下のコメントで @Stefan が提案した String[Regexp, Integer] 構文を使用します。

regexes.lazy.filter_map { |regex| p regex; string[regex, 1] }.first

デモ:

irb> regexes = [/(1)/, /(2)/, /(3)/]
=> [/(1)/, /(2)/, /(3)/]
irb> string = "2"
=> "2"
irb> regexes.lazy.filter_map { |regex| p regex; string[regex, 1] }.first
/(1)/
/(2)/
=> "2"
irb> string = "4"
=> "4"
irb> regexes.lazy.filter_map { |regex| p regex; string[regex, 1] }.first
/(1)/
/(2)/
/(3)/
=> nil