2015-10-04 6 views
7

私はそこにたくさんの文字列があるテキストファイルを持っています。私は、ストリームを使用してJava 8に相当するものを実装するにはどうすればよいgrepのようなjava 8ストリームからマッチングする前後の行を取得する方法は?

grep -A 10 -B 10 "ABC" myfile.txt 

:私はgrepの中に一致する前と後の行を検索したい場合は、私はこのように行うのでしょうか?

+0

の背後にある会社のために働きます。 –

答えて

2

このようなシナリオは、既存のメソッドがストリーム内の要素ネイバーへのアクセスを提供しないため、Stream APIではうまくサポートされていません。私はカスタムイテレータ/ spliteratorsとサードパーティのライブラリ呼び出しを作成せずに、最大考えることができる最も近いソリューションはListに入力ファイルを読み込んで、ストリームインデックスを使用することです:

List<String> input = Files.readAllLines(Paths.get(fileName)); 
Predicate<String> pred = str -> str.contains("ABC"); 
int contextLength = 10; 

IntStream.range(0, input.size()) // line numbers 
    // filter them leaving only numbers of lines satisfying the predicate 
    .filter(idx -> pred.test(input.get(idx))) 
    // add nearby numbers 
    .flatMap(idx -> IntStream.rangeClosed(idx-contextLength, idx+contextLength)) 
    // remove numbers which are out of the input range 
    .filter(idx -> idx >= 0 && idx < input.size()) 
    // sort numbers and remove duplicates 
    .distinct().sorted() 
    // map to the lines themselves 
    .mapToObj(input::get) 
    // output 
    .forEachOrdered(System.out::println); 

grepの出力も"--"のような特殊な区切り文字を含んでいます省略された行を指定します。 Tagir Valeevが指摘したように、この種の

// Same as IntStream.range(...).filter(...) steps above 
IntStreamEx.ofIndices(input, pred) 
    // same as above 
    .flatMap(idx -> IntStream.rangeClosed(idx-contextLength, idx+contextLength)) 
    // remove numbers which are out of the input range 
    .atLeast(0).less(input.size()) 
    // sort numbers and remove duplicates 
    .distinct().sorted() 
    .boxed() 
    // merge adjacent numbers into single interval and map them to subList 
    .intervalMap((i, j) -> (j - i) == 1, (i, j) -> input.subList(i, j + 1)) 
    // flatten all subLists prepending them with "--" 
    .flatMap(list -> StreamEx.of(list).prepend("--")) 
    // skipping first "--" 
    .skip(1) 
    .forEachOrdered(System.out::println); 
1

:あなたはさらに行くと、同様な動作を模倣したい場合は、この場合に便利ですintervalMap方法を持っているとして、私は私の自由StreamExライブラリをしようとするあなたを提案することができます問題はストリームAPIでうまくサポートされていません。インクリメンタルに入力から行を読み込み、一致する行をコンテキストで表示したい場合は、ステートフルなパイプラインステージ(またはカスタムコレクタまたはスプライテータ)を導入する必要があります。

すべての行をメモリに読み込む場合は、BitSetがマッチのグループを操作するのに便利です。これはTagirの解と類似していますが、整数範囲を使用して印刷する線を表現する代わりに、BitSetの1ビットを使用します。 BitSetのいくつかの利点は、多数の組み込みの一括操作があり、内部表現がコンパクトであることです。また、1ビットのインデックスのストリームを生成することもできます。これは、この問題に非常に役立ちます。

まずは、述語に一致する行ごとに1ビットの持つBitSet作成することから始めてみましょう:

void contextMatch(Predicate<String> pred, int before, int after, List<String> input) { 
    int len = input.size(); 
    BitSet matches = IntStream.range(0, len) 
           .filter(i -> pred.test(input.get(i))) 
           .collect(BitSet::new, BitSet::set, BitSet::or); 

を今、私たちはマッチした行のビットセットを持っていることを、私たちはインデックスをストリーム各1ビットの次に、前後のコンテキストを表すビットセット内のビットを設定します。これにより、1ビットがコンテキスト行を含めて印刷されるすべての行を表す単一のBitSetが得られます。

BitSet context = matches.stream() 
     .collect(BitSet::new, 
       (bs,i) -> bs.set(Math.max(0, i - before), Math.min(i + after + 1, len)), 
       BitSet::or); 

我々だけでコンテキストを含むすべての行を、印刷したい場合は、我々はこれを行うことができます:

context.stream() 
      .forEachOrdered(i -> System.out.println(input.get(i))); 

実際grep -A a -B bコマンドは、コンテキスト行の各グループ間の区切りを印刷します。セパレータをいつ印刷するかを理解するために、コンテキストビットセット内の各1ビットを見る。その前に0ビットがある場合、または最初にある場合は、結果にビットを設定します。これは、コンテキスト行の各グループの先頭に私たちに1ビットを与える:

私たちは、コンテキスト行の各グループの前に区切りを印刷する必要はありません。それぞれのグループにの間にを印刷します。つまり、最初の1ビット(ある場合)をクリアする必要があります。

// clear the first bit 
    int first = separators.nextSetBit(0); 
    if (first >= 0) { 
     separators.clear(first); 
    } 

ここで結果の行を出力できます。しかし、それぞれの行を印刷する前に、我々は最初の区切り文字を印刷する必要があるかどうかを確認します。あなたは、サードパーティのライブラリを使用するために喜んでいると並列処理を必要としない場合

context.stream() 
      .forEachOrdered(i -> { 
       if (separators.get(i)) { 
        System.out.println("--"); 
       } 
       System.out.println(input.get(i)); 
      }); 
} 
+0

興味深いアプローチ、upvoted。もう一つの方法は、最初の2つのステップを 'IntStream.range(..)。filter(..)。flatMap(..)。filter(..)'のステップをマージして、 '.collect(BitSet ::新しい、BitSet :: set、BitSet :: or) '.distinct()。sorted()'の代わりに。これはメモリ効率を維持する一方で、よりストリーに見えるかもしれません。 Btw 'i> 0 &&!context.get(i-1)|| i == 0'は 'i == 0 ||に短縮することができる。 !context.get(i-1) '。 –

+2

私はあなたの中間ステップを簡素化しました。私はあなたが私がそれを直接編集したことに気にしないことを願っています。その文脈では分かりやすいが、私にはコメントが難しいように見えた。 – Holger

+0

@TagirValeevあなたの "btw"に良い提案。私は 'i == 0'の場合を追加してそのエッジケースを手に入れました。編集されました。 –

4

、そしてjOOλは、SQLスタイルのウィンドウを提供しています機能、

-1:  .window(-1, 1) 
0:  .filter(w -> w.value().contains("ABC")) 
+1:  .forEach(w -> { 
-1:   System.out.println("+1:" + w.lead().orElse("")); 
0:   // ABC: Just checking 
+1:  }); 

lead()関数はウィンドウからトラバーサル順で次の値をアクセス0降伏

Seq.seq(Files.readAllLines(Paths.get(new File("/path/to/Example.java").toURI()))) 
    .window(-1, 1) 
    .filter(w -> w.value().contains("ABC")) 
    .forEach(w -> { 
     System.out.println("-1:" + w.lag().orElse("")); 
     System.out.println(" 0:" + w.value()); 
     System.out.println("+1:" + w.lead().orElse("")); 
     // ABC: Just checking 
    }); 

を次のように関数は前の行にアクセスします。

免責事項:私は残念ながら、箱から出しストリームAPIでサポートされていないですが、何がしたいことは、「スライディングウィンドウ」と呼ばれているjOOλ

関連する問題