Bug 548587 - Performance improvement for SelectionUtils

Change-Id: I39650ce929df946260a09f342a985bc6c78c57ee
Signed-off-by: Dirk Fauth <dirk.fauth@googlemail.com>
diff --git a/org.eclipse.nebula.widgets.nattable.core.test/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtilTest.java b/org.eclipse.nebula.widgets.nattable.core.test/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtilTest.java
index 76011cd..0f34858 100644
--- a/org.eclipse.nebula.widgets.nattable.core.test/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtilTest.java
+++ b/org.eclipse.nebula.widgets.nattable.core.test/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtilTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2012, 2018 Original authors and others.
+ * Copyright (c) 2012, 2019 Original authors and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -11,6 +11,7 @@
 package org.eclipse.nebula.widgets.nattable.coordinate;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
@@ -102,4 +103,45 @@
         }
     }
 
+    @Test
+    public void shouldJoinContiguousRanges() {
+        Range r1 = new Range(2, 5);
+        Range r2 = new Range(5, 9);
+        Range r3 = new Range(9, 12);
+
+        assertEquals(new Range(2, 12), PositionUtil.joinConsecutiveRanges(Arrays.asList(r1, r2, r3)));
+    }
+
+    @Test
+    public void shouldNotJoinNotContiguousRanges() {
+        Range r1 = new Range(2, 5);
+        Range r2 = new Range(6, 9);
+        Range r3 = new Range(10, 12);
+
+        assertNull(PositionUtil.joinConsecutiveRanges(Arrays.asList(r1, r2, r3)));
+    }
+
+    @Test
+    public void shouldMergeContiguousRangesToOne() {
+        Range r1 = new Range(2, 5);
+        Range r2 = new Range(4, 9);
+        Range r3 = new Range(7, 12);
+
+        List<Range> mergedRanges = PositionUtil.mergeRanges(Arrays.asList(r1, r2, r3));
+        assertEquals(1, mergedRanges.size());
+        assertEquals(new Range(2, 12), mergedRanges.get(0));
+    }
+
+    @Test
+    public void shouldMergeNotContiguousRanges() {
+        Range r1 = new Range(2, 5);
+        Range r2 = new Range(4, 9);
+        Range r3 = new Range(10, 12);
+        Range r4 = new Range(12, 15);
+
+        List<Range> mergedRanges = PositionUtil.mergeRanges(Arrays.asList(r1, r2, r3, r4));
+        assertEquals(2, mergedRanges.size());
+        assertEquals(new Range(2, 9), mergedRanges.get(0));
+        assertEquals(new Range(10, 15), mergedRanges.get(1));
+    }
 }
diff --git a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtil.java b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtil.java
index fbf4313..731de16 100644
--- a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtil.java
+++ b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/coordinate/PositionUtil.java
@@ -16,9 +16,11 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.TreeSet;
 
 import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
 
@@ -170,6 +172,73 @@
     }
 
     /**
+     * Join a set of ranges if they describe a consecutive range when combined.
+     *
+     * @param ranges
+     *            Collection of {@link Range}s that should be joined.
+     * @return The joined {@link Range} or <code>null</code> if {@link Range}s
+     *         do not describe a consecutive {@link Range} when combined.
+     *
+     * @since 1.6
+     */
+    public static Range joinConsecutiveRanges(Collection<Range> ranges) {
+        if (ranges == null || ranges.isEmpty()) {
+            return null;
+        }
+
+        // put to list
+        List<Range> sortedRanges = new ArrayList<Range>(ranges);
+
+        // sort by 1) start, 2) end position
+        Collections.sort(sortedRanges, new Comparator<Range>() {
+
+            @Override
+            public int compare(Range o1, Range o2) {
+                if (o1.start == o2.start) {
+                    return Integer.compare(o1.end, o2.end);
+                } else {
+                    return Integer.compare(o1.start, o2.start);
+                }
+            }
+        });
+
+        int start = sortedRanges.get(0).start;
+        int end = sortedRanges.get(0).end;
+        for (int i = 1; i < sortedRanges.size(); i++) {
+            Range range = sortedRanges.get(i);
+            if (range.start > end) {
+                return null;
+            }
+            end = Integer.max(end, range.end);
+        }
+
+        return new Range(start, end);
+    }
+
+    /**
+     * Takes a collection of {@link Range}s and merges them to get
+     * {@link Range}s without overlapping. If there are no gaps between the
+     * {@link Range}s a single Range will be the result, otherwise multiple
+     * {@link Range}s will be in the resulting collection.
+     *
+     * @param ranges
+     *            The {@link Range}s to merge.
+     * @return Collection of {@link Range}s without overlapping.
+     *
+     * @since 1.6
+     */
+    public static List<Range> mergeRanges(Collection<Range> ranges) {
+        Set<Integer> numbers = new TreeSet<Integer>();
+        for (Range range : ranges) {
+            for (int number = range.start; number < range.end; number++) {
+                numbers.add(number);
+            }
+        }
+
+        return getRanges(numbers);
+    }
+
+    /**
      * Calculates the horizontal move direction based on the from and to column
      * position.
      *
diff --git a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/selection/SelectionUtils.java b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/selection/SelectionUtils.java
index db0a43e..b6f9c27 100644
--- a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/selection/SelectionUtils.java
+++ b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/selection/SelectionUtils.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2012, 2018 Original authors and others.
+ * Copyright (c) 2012, 2019 Original authors and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -13,13 +13,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
-import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
+import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil;
 import org.eclipse.nebula.widgets.nattable.coordinate.Range;
 import org.eclipse.nebula.widgets.nattable.data.IRowDataProvider;
 import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
@@ -110,50 +107,35 @@
      * @since 1.4
      */
     public static boolean hasConsecutiveSelection(SelectionLayer selectionLayer) {
-        if (selectionLayer != null
-                && SelectionUtils.isConsecutive(selectionLayer.getSelectedColumnPositions())) {
+        if (selectionLayer == null) {
+            return false;
+        }
 
-            Map<Integer, Set<Integer>> positions = new HashMap<Integer, Set<Integer>>();
-            int column = -1;
-            int row = -1;
+        int[] selectedColumnPositions = selectionLayer.getSelectedColumnPositions();
+        if (selectedColumnPositions.length == 0) {
+            return false;
+        }
 
-            // collect the selection information
-            for (PositionCoordinate coord : selectionLayer.getSelectedCellPositions()) {
-                Set<Integer> rows = positions.get(coord.columnPosition);
-                if (rows == null) {
-                    rows = new LinkedHashSet<Integer>();
-                    positions.put(coord.columnPosition, rows);
-                }
-                rows.add(coord.rowPosition);
+        Arrays.sort(selectedColumnPositions);
 
-                column = Math.max(column, coord.columnPosition);
-                row = Math.max(row, coord.rowPosition);
+        Set<Range> selectedRowPositions = selectionLayer.getSelectedRowPositions();
+        Range range = PositionUtil.joinConsecutiveRanges(selectedRowPositions);
+        if (range == null) {
+            return false;
+        }
+
+        for (int colPosIdx = 0; colPosIdx < selectedColumnPositions.length; colPosIdx++) {
+            if (colPosIdx > 0 && selectedColumnPositions[colPosIdx - 1] + 1 != selectedColumnPositions[colPosIdx]) {
+                return false;
             }
-
-            // check if the selected region is a rectangle
-            // every row collection for each column needs to have the same size
-            // and the same content
-            Set<Integer> previous = null;
-            for (Set<Integer> rows : positions.values()) {
-                if (previous != null && !previous.equals(rows)) {
+            final int columnPosition = selectedColumnPositions[colPosIdx];
+            for (int rowPositionIndex = range.start; rowPositionIndex < range.end; rowPositionIndex++) {
+                if (!selectionLayer.isCellPositionSelected(columnPosition, rowPositionIndex)) {
                     return false;
                 }
-                previous = rows;
-            }
-
-            // test if rows are consecutive
-            if (previous != null) {
-                int[] rowPositions = new int[previous.size()];
-                int i = 0;
-                for (Integer rowPos : previous) {
-                    rowPositions[i++] = rowPos;
-                }
-                if (SelectionUtils.isConsecutive(rowPositions)) {
-                    return true;
-                }
             }
         }
-        return false;
+        return true;
     }
 
     /**
@@ -170,16 +152,34 @@
      * @since 1.4
      */
     public static ILayerCell getBottomRightCellInSelection(SelectionLayer selectionLayer) {
-        if (hasConsecutiveSelection(selectionLayer)) {
-            int column = -1;
-            int row = -1;
-            for (PositionCoordinate coord : selectionLayer.getSelectedCellPositions()) {
-                column = Math.max(column, coord.columnPosition);
-                row = Math.max(row, coord.rowPosition);
-            }
-            return selectionLayer.getCellByPosition(column, row);
+        if (selectionLayer == null) {
+            return null;
         }
-        return null;
+
+        int[] selectedColumnPositions = selectionLayer.getSelectedColumnPositions();
+        Arrays.sort(selectedColumnPositions);
+
+        Set<Range> selectedRowPositions = selectionLayer.getSelectedRowPositions();
+        Range range = PositionUtil.joinConsecutiveRanges(selectedRowPositions);
+        if (range == null) {
+            return null;
+        }
+
+        for (int colPosIdx = 0; colPosIdx < selectedColumnPositions.length; colPosIdx++) {
+            if (colPosIdx > 0 && selectedColumnPositions[colPosIdx - 1] + 1 != selectedColumnPositions[colPosIdx]) {
+                return null;
+            }
+            final int columnPosition = selectedColumnPositions[colPosIdx];
+            for (int rowPositionIndex = range.start; rowPositionIndex < range.end; rowPositionIndex++) {
+                if (!selectionLayer.isCellPositionSelected(columnPosition, rowPositionIndex)) {
+                    return null;
+                }
+            }
+        }
+
+        int colPosition = selectedColumnPositions[selectedColumnPositions.length - 1];
+        int rowPosition = range.end - 1;
+        return selectionLayer.getCellByPosition(colPosition, rowPosition);
     }
 
     /**