Support comparison of integer values

Change-Id: I5b8718165579a4606b3beb1635ea71eb5d2619f9
diff --git a/Changes b/Changes
index e101056..d0bac00 100644
--- a/Changes
+++ b/Changes
@@ -1,8 +1,10 @@
-0.63.3 2024-11-04
+0.63.3 2024-12-15
     - [performance] Improve short circuit on count=0 and
       cutoff=true (diewald)
     - [feature] Make defaultSearchContextLength and maxCharContextSize 
-      customizable (margaretha)  
+      customizable (margaretha)
+    - [feature] Support comparison of integer values
+      (diewald & kupietz)
 
 0.63.2 2024-08-02
     - [bugfix] Fix empty DocIdSetIterator (margaretha)
diff --git a/src/main/java/de/ids_mannheim/korap/KrillCollection.java b/src/main/java/de/ids_mannheim/korap/KrillCollection.java
index ffa99cd..dd4cb75 100644
--- a/src/main/java/de/ids_mannheim/korap/KrillCollection.java
+++ b/src/main/java/de/ids_mannheim/korap/KrillCollection.java
@@ -296,7 +296,36 @@
                 };
 
                 throw new QueryException(841,
-                        "Match relation unknown for type");
+                        "Match relation `" + match+ "' unknown for type:date");
+            }
+
+            // Filter based on integer
+            else if (valtype.equals("type:integer")) {
+
+                if (!json.has("value"))
+                    throw new QueryException(820, "Integers require value fields");
+
+                int value = json.get("value").asInt();
+
+                if (json.has("match")) match = json.get("match").asText();
+
+                switch (match) {
+                    case "match:geq":
+                        return this.cb.geq(key, value);
+                    case "match:leq":
+                        return this.cb.leq(key, value);
+                    case "match:eq":
+                        return this.cb.eq(key, value);
+                    case "match:ne":
+                        return this.cb.eq(key, value).not();
+                    case "match:gt":
+                        return this.cb.gt(key, value);
+                    case "match:lt":
+                        return this.cb.lt(key, value);
+                };
+
+                throw new QueryException(841,
+                        "Match relation `" + match+ "' unknown for type:integer");
             }
 
             // Filter based on string
diff --git a/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java b/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java
index 166dc90..bc3c7ff 100644
--- a/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java
+++ b/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java
@@ -71,7 +71,7 @@
 
         return new CollectionBuilder.Range(field, since, KrillDate.END);
     };
-
+    
 	public CollectionBuilder.Interface nothing () {
 
 		// Requires that a field with name "0---" does not exist
@@ -109,6 +109,38 @@
         return this.andGroup().with(startObj).with(endObj);
     };
 
+    public CollectionBuilder.Interface leq (String field, Integer end) {
+        return this.between(field, Integer.MIN_VALUE, end);
+    };
+
+    public CollectionBuilder.Interface geq (String field, Integer start) {
+        return this.between(field, start, Integer.MAX_VALUE);
+    };
+
+    public CollectionBuilder.Interface eq (String field, Integer value) {
+        return this.between(field, value, value);
+    };
+
+    public CollectionBuilder.Interface lt (String field, Integer value) {
+        return this.between(field, Integer.MIN_VALUE, value - 1);
+    };
+
+    public CollectionBuilder.Interface gt (String field, Integer value) {
+        return this.between(field, value + 1, Integer.MAX_VALUE);
+    };
+
+    // This will be optimized away in future versions
+    public CollectionBuilder.Interface between (String field, Integer start,
+            Integer end) {
+
+        try {
+            return new CollectionBuilder.NumRange(field, start, end);
+        }
+        catch (NumberFormatException e) {
+            log.warn("Parameter of between(int,int) is invalid");
+        };
+        return null;
+    };
 
     public CollectionBuilder.Interface date (String field, String date) {
         KrillDate dateDF = new KrillDate(date);
@@ -427,4 +459,41 @@
             return this;
         };
     };
+
+    public class NumRange implements CollectionBuilder.Interface {
+        private boolean isNegative = false;
+        private String field;
+        private int start, end;
+
+        public NumRange (String field, int start, int end) {
+            this.field = field;
+            this.start = start;
+            this.end = end;
+        };
+
+
+        public boolean isNegative () {
+            return this.isNegative;
+        };
+
+
+        public String toString () {
+            Filter filter = this.toFilter();
+            if (filter == null)
+                return "";
+            return filter.toString();
+        };
+
+
+        public Filter toFilter () {
+            return NumericRangeFilter.newDoubleRange(this.field, (double) this.start,
+                                                     (double) this.end, true, true);
+        };
+
+
+        public CollectionBuilder.Interface not () {
+            this.isNegative = true;
+            return this;
+        };
+    };
 };
diff --git a/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java b/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java
index c5d87f9..066dc43 100644
--- a/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java
+++ b/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java
@@ -586,6 +586,54 @@
         assertEquals(1, kcn.docCount());
 	};
 
+    @Test
+    public void testIndexWithIntegers () throws IOException {
+        ki = new KrillIndex();
+
+        FieldDocument fd = ki.addDoc(createDoc1());
+        ki.addDoc(createDoc2());
+        ki.addDoc(createDoc5001());
+        ki.commit();
+
+        CollectionBuilder cb = new CollectionBuilder();
+        KrillCollection kcn = new KrillCollection(ki);
+
+        assertEquals("toks:[2000.0 TO 4000.0]", cb.between("toks", 2000, 4000).toString());
+
+        kcn.fromBuilder(cb.between("toks", 2000, 4000));
+        assertEquals(1, kcn.docCount());
+
+        kcn.fromBuilder(cb.geq("toks", 2000));
+        assertEquals(1, kcn.docCount());
+
+        kcn.fromBuilder(cb.leq("toks", 4000));
+        assertEquals(1, kcn.docCount());
+
+        kcn.fromBuilder(cb.leq("toks", 2000));
+        assertEquals(0, kcn.docCount());
+
+        kcn.fromBuilder(cb.geq("toks", 4000));
+        assertEquals(0, kcn.docCount());
+
+        kcn.fromBuilder(cb.lt("toks", 3000));
+        assertEquals(0, kcn.docCount());
+
+        kcn.fromBuilder(cb.lt("toks", 3001));
+        assertEquals(1, kcn.docCount());
+
+        kcn.fromBuilder(cb.gt("toks", 3000));
+        assertEquals(0, kcn.docCount());
+
+        kcn.fromBuilder(cb.gt("toks", 2999));
+        assertEquals(1, kcn.docCount());
+
+        kcn.fromBuilder(cb.eq("toks", 3000));
+        assertEquals(1, kcn.docCount());
+
+        kcn.fromBuilder(cb.eq("toks", 3001));
+        assertEquals(0, kcn.docCount());
+    };
+
 	@Test
     public void testIndexWithTextStringQueries () throws IOException {
 		ki = new KrillIndex();
@@ -1241,6 +1289,18 @@
         return fd;
     };
 
+    public static FieldDocument createDoc5001 () {
+        FieldDocument fd = new FieldDocument();
+        fd.addString("UID", "5001");
+		fd.addString("ID", "doc-5001");
+        fd.addInt("toks", 3000);
+        fd.addDate("pubDate", 20180202);
+        fd.addText("text", "Der alte  Mann ging über die Straße");
+        fd.addTV("tokens", "a b c", "[(0-1)s:a|i:a|_0$<i>0<i>1|-:t$<i>3]"
+				 + "[(2-3)s:b|i:b|_1$<i>2<i>3]" + "[(4-5)s:c|i:c|_2$<i>4<i>5]");
+        return fd;
+    };
+
     private String _getJSONString (String file) {
         return getJsonString(getClass().getResource(path + file).getFile());
     };