Index: lucene/src/test/org/apache/lucene/search/TestFreqFilteringScorerWrapper.java
===================================================================
--- lucene/src/test/org/apache/lucene/search/TestFreqFilteringScorerWrapper.java	(revision 0)
+++ lucene/src/test/org/apache/lucene/search/TestFreqFilteringScorerWrapper.java	(revision 0)
@@ -0,0 +1,262 @@
+package org.apache.lucene.search;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.lucene.analysis.MockAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.IndexReader.AtomicReaderContext;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.SlowMultiReaderWrapper;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.Weight.ScorerContext;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util._TestUtil;
+
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.After;
+
+public class TestFreqFilteringScorerWrapper extends LuceneTestCase {
+
+  private static final String FIELD = "field";
+  private static final String CONST_VALUE = "const";
+  private static final int MIN_TERM = 1;
+  private static final int MAX_TERM = 20;
+  private static final int NUM_VALUES_TARGET = MAX_TERM - MIN_TERM + 1;
+  
+  protected Directory directory;
+  protected IndexSearcher indexSearcher;
+  protected IndexReader indexReader;
+ 
+  @Before
+  public void buildIndex() throws Exception {
+    directory = newDirectory();
+    
+    RandomIndexWriter writer = 
+      new RandomIndexWriter(random, directory, newIndexWriterConfig
+                            (TEST_VERSION_CURRENT, 
+                             new MockAnalyzer(random)).setMergePolicy
+                            (newLogMergePolicy()));
+
+    final int numDocs = atLeast(random, 1000);
+    for (int i = 0; i < numDocs; i++) {
+      Document doc = new Document();
+      doc.add(newField(FIELD, CONST_VALUE, 
+                       Field.Store.NO, Field.Index.NOT_ANALYZED));
+      if (usually(random)) {
+
+        final int numVals = _TestUtil.nextInt
+          (random, NUM_VALUES_TARGET / 2, NUM_VALUES_TARGET);
+
+        for (int j = 0; j < numVals; j++) {
+          doc.add(newField(FIELD, ""+_TestUtil.nextInt(random, MIN_TERM, MAX_TERM),
+                           Field.Store.NO, Field.Index.NOT_ANALYZED));
+        } 
+      }
+      writer.addDocument(doc);
+    }
+    indexReader = new SlowMultiReaderWrapper(writer.getReader());
+    writer.close();
+    indexSearcher = newSearcher(indexReader);
+
+    assertTrue(indexSearcher.getTopReaderContext().isAtomic);
+  }
+  
+  @After
+  public void closeIndex() throws Exception {
+    indexSearcher.close();
+    indexReader.close();
+    directory.close();
+  }
+
+  @Test
+  public void testEdgeCases() throws IOException {
+    final int maxDoc = indexSearcher.maxDoc();
+
+    // -/+ Inf for min/max when we have matches
+
+    Scorer matchAll = new FreqFilteringScorerWrapper
+      (getScorer(new TermQuery(new Term(FIELD, CONST_VALUE))),
+       Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
+    
+    final float[] allOnes = collectFreqs(matchAll, maxDoc, null);
+
+    for (int d = 0; d < allOnes.length; d++ ) {
+      assertEquals("allOnes wasn't 1 for doc#"+d,
+                   1.0f, allOnes[d], 0.0f);
+    }
+
+    // wrap a scorer that matches nothing and make sure we also match nothing
+
+    // :nocommit: ... NPE because inner scorer optimizes itself to null?
+    Scorer matchNothing =  new FreqFilteringScorerWrapper
+      (getScorer(new TermQuery(new Term("Not really a field", 
+                                        "not really a term"))),
+       Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
+    
+    final float[] nothing = collectFreqs(matchNothing, maxDoc, null);
+
+    for (int d = 0; d < nothing.length; d++ ) {
+      assertEquals("nothing contained something for doc#"+d,
+                   Float.NaN, nothing[d], 0.0f);
+    }
+
+  }
+
+  @Test
+  public void testRandom() throws IOException {
+    for (int i = MIN_TERM; i <= MAX_TERM; i++) {
+      testTerm(new Term(FIELD, ""+i));
+    };
+  }
+
+  /** randomized testing of a term */
+  protected void testTerm(Term t) throws IOException {
+    final int maxDoc = indexSearcher.maxDoc();
+    final Query q = new TermQuery(t);
+
+    // first record the results of an unfiltered collection
+
+    final float[] unfilteredFreqs = collectFreqs(getScorer(q), maxDoc, null);
+
+    // now pick some random freq filter values and assert we get what we expect
+
+    final int numFilteredIters = atLeast(random, 5 * NUM_VALUES_TARGET);
+    for (int i = 0; i < numFilteredIters; i++ ) {
+
+      final int minFreq = _TestUtil.nextInt(random, 
+                                            0, NUM_VALUES_TARGET / 2);
+      final int maxFreq = _TestUtil.nextInt(random, 
+                                            minFreq, 2 * NUM_VALUES_TARGET);
+
+      Scorer fs = new FreqFilteringScorerWrapper(getScorer(q),
+                                                 (float)minFreq,
+                                                 (float)maxFreq);
+      final float[] freqs = collectFreqs(fs, maxDoc, unfilteredFreqs);
+
+      for (int d = 0; d < maxDoc; d++) {
+        if (Float.isNaN(unfilteredFreqs[d])) {
+          continue;
+        }
+        String msg = t+" - doc: "+d+", minFreq="+minFreq+", maxFreq="+maxFreq;
+        if (unfilteredFreqs[d] < minFreq || maxFreq < unfilteredFreqs[d]) {
+          assertEquals(msg, Float.NaN, freqs[d], 0.0f);
+        } else {
+          assertEquals(msg, unfilteredFreqs[d], freqs[d], 0.0f);
+        }
+      }
+    }
+
+  }
+
+  // :nocommit: I don't really understand this, i cut/paste from TestTermScorer under the impression it was the "safe" way to get a scorer from a query such that it could then be used in a test .. something about it i don't understand is causing the NPE mentioned above
+  private Scorer getScorer(final Query q) throws IOException {
+    return indexSearcher.createNormalizedWeight(q).scorer((AtomicReaderContext)indexSearcher.getTopReaderContext(), ScorerContext.def().scoreDocsInOrder(true).topScorer(true));
+  }
+
+  private float[] nanArray(int size) {
+    float[] freqs = new float[size];
+    Arrays.fill(freqs, Float.NaN);
+    return freqs;
+  }
+
+  /**
+   * randomly decides to either call score(Collector) on a FreqCollector
+   * or randomly alternates between nextId and advance
+   * if baselineFreqs is non null, it will be used to decide how far ahead 
+   * of docId() can be used if advance will be called (if it is null, all 
+   * calls to advance will be advance(docId+1);
+   */
+  private float[] collectFreqs(final Scorer s, final int maxDoc, 
+                               final float[] baselineFreqs) throws IOException {
+
+    // :TODO: don't we already have a standard test util that does crazy random alternating next/advance stuff?
+    
+    final float[] freqs = nanArray(maxDoc);
+    final Collector col = new FreqCollector(freqs);
+
+    if (random.nextBoolean()) {
+      s.score(col);
+    } else {
+      col.setScorer(s);
+      int doc = s.nextDoc();
+      while (Scorer.NO_MORE_DOCS != doc) {
+        col.collect(doc);
+        if (random.nextBoolean()) {
+          doc = s.nextDoc();
+        } else {
+          int next = doc+1;
+          if (null != baselineFreqs) {
+            next = _TestUtil.nextInt
+              (random, next, findNextNonNaN(baselineFreqs, next));
+          }
+          doc = s.advance(next);
+        }
+      }
+    }
+    return freqs;
+  }
+
+  /** 
+   * returns the lowest index of the data array which is greater then 
+   * or equal to idx such that the value of the data is not NaN)
+   * data is exhausted w/o finding a non-NaN value, data.length
+   * will be returned.
+   */
+  private int findNextNonNaN(final float[] data, final int idx) {
+    for (int i = idx; i < data.length; i++) {
+      if (! Float.isNaN(data[i]) ) return i;
+    }
+    return data.length;
+  }
+  
+  private static class FreqCollector extends Collector {
+    private int base = 0;
+    private Scorer scorer;
+    private final float[] freqs;
+    public FreqCollector(final float[] freqs) {
+      this.freqs = freqs;
+    }
+    
+    @Override
+    public void setScorer(Scorer scorer) throws IOException {
+      this.scorer = scorer;
+    }
+    
+    @Override
+    public void collect(int doc) throws IOException {
+      freqs[doc + base] = scorer.freq();
+    }
+    
+    @Override
+    public void setNextReader(AtomicReaderContext context) {
+      base = context.docBase;
+    }
+    
+    @Override
+    public boolean acceptsDocsOutOfOrder() {
+      return true;
+    }
+  }
+}

Property changes on: lucene/src/test/org/apache/lucene/search/TestFreqFilteringScorerWrapper.java
___________________________________________________________________
Added: svn:keywords
   + Date Author Id Revision HeadURL
Added: svn:eol-style
   + native

Index: lucene/src/java/org/apache/lucene/search/FreqFilteringScorerWrapper.java
===================================================================
--- lucene/src/java/org/apache/lucene/search/FreqFilteringScorerWrapper.java	(revision 0)
+++ lucene/src/java/org/apache/lucene/search/FreqFilteringScorerWrapper.java	(revision 0)
@@ -0,0 +1,98 @@
+package org.apache.lucene.search;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * <p>
+ * Wraps another Scorer and only collects/scores documents which would 
+ * be collected by that scorer and have a frequency within the specified 
+ * range.
+ * </p>
+ * <p>
+ * This class relies on an experimental feature of the Scorer API, therfore 
+ * it is (in it's entirety) experimental.
+ * </p> 
+ * @lucene.experimental
+ */
+public class FreqFilteringScorerWrapper extends Scorer {
+  
+  protected final float minFreq;
+  protected final float maxFreq;
+  protected final Scorer inner;
+  protected final Collection<ChildScorer> innerChild;
+
+  public FreqFilteringScorerWrapper(Scorer inner, float minFreq, float maxFreq) {
+    super(inner.getWeight());
+    this.minFreq = minFreq;
+    this.maxFreq = maxFreq;
+    this.inner = inner;
+    innerChild = Collections.singleton(new ChildScorer(inner,"wrapped"));
+  }
+  
+  @Override
+  public int docID() {
+    return inner.docID();
+  }
+  
+  @Override
+  public int nextDoc() throws IOException {
+
+    int doc = inner.nextDoc();
+
+    while (NO_MORE_DOCS != doc) {
+      float freq = inner.freq();
+      if (minFreq <= freq && freq <= maxFreq) break;
+      doc = inner.nextDoc();
+    }
+
+    return doc;
+  }
+
+  @Override
+  public int advance(int target) throws IOException {
+
+    int doc = inner.advance(target);
+
+    while (NO_MORE_DOCS != doc) {
+      float freq = inner.freq();
+      if (minFreq <= freq && freq <= maxFreq) break;
+      doc = inner.nextDoc();
+    }
+
+    return doc;
+  }
+
+  @Override
+  public float score() throws IOException {
+    return inner.score();
+  }
+
+  @Override
+  public float freq() throws IOException {
+    return inner.freq();
+  }
+  
+  @Override
+  public Collection<ChildScorer> getChildren() {
+    return innerChild;
+  }
+}

Property changes on: lucene/src/java/org/apache/lucene/search/FreqFilteringScorerWrapper.java
___________________________________________________________________
Added: svn:keywords
   + Date Author Id Revision HeadURL
Added: svn:eol-style
   + native

