Repository: jtablesaw/tablesaw (java)

Issue:
java.lang.OutOfMemoryError when appending a column to itself
Appending a column to itself causes a `java.lang.OutOfMemoryError`

Code example:

```java
final DoubleColumn c1 = DoubleColumn.create("c1", new double[]{42});
final DoubleColumn c2 = DoubleColumn.create("c2", new double[]{9001});

final DoubleColumn c1Copy = c1.copy();
final DoubleColumn c2Copy = c2.copy();

c1.append(c2); // works
c2.append(c1); // works

c1.append(c1Copy); // works
c2.append(c2Copy); // works

c1.append(c1); // java.lang.OutOfMemoryError
c2.append(c2); // java.lang.OutOfMemoryError
```

Stacktrace:

```
java.lang.OutOfMemoryError: Java heap space

	at it.unimi.dsi.fastutil.doubles.DoubleArrays.forceCapacity(DoubleArrays.java:122)
	at it.unimi.dsi.fastutil.doubles.DoubleArrayList.grow(DoubleArrayList.java:244)
	at it.unimi.dsi.fastutil.doubles.DoubleArrayList.add(DoubleArrayList.java:259)
	at tech.tablesaw.api.DoubleColumn.append(DoubleColumn.java:229)
	at tech.tablesaw.api.DoubleColumn.append(DoubleColumn.java:298)
```

The problem is in `DoubleColumn.java`, line 297: 

```java
for (int i = 0; i < numberColumn.size(); i++) {
    append(numberColumn.getDouble(i));
}
```

Using `numberColumn.size()` in the condition for the for loop and then appending to the same column in the for loop will cause the column to grow until the JVM runs out of heap space.

EDIT: It seems like this problem also occurs in IntColumn, LongColumn, DateColumn, etc...

A simple fix would be: 

```java
final int size = numberColumn.size();
for (int i = 0; i < size; i++) {
    append(numberColumn.getDouble(i));
}
```

But it would have to be applied to most if not all classes implementing the `Column` interface. I feel like a more generic solution with less code duplication should be preferred, though.

Pull request description:
Fix #390
- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Determine the size of the column about to be appended outside of the for loop that actually appends the two columns. This way, appending a column to itself will not leat to a `java.lang.OutOfMemoryError`. See #390 for more details

## Testing

See `core/src/test/java/tech/tablesaw/columns/ColumnAppendTest.java`


Patch:
diff --git a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
index 886c0a34..7c3a32f7 100644
--- a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
@@ -590,7 +590,8 @@ public class BooleanColumn extends AbstractColumn<Boolean> implements BooleanMap
     public BooleanColumn append(Column<Boolean> column) {
         checkArgument(column.type() == this.type());
         BooleanColumn col = (BooleanColumn) column;
-        for (int i = 0; i < column.size(); i++) {
+        final int size = col.size();
+        for (int i = 0; i < size; i++) {
             append(col.getByte(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/DateColumn.java b/core/src/main/java/tech/tablesaw/api/DateColumn.java
index 942eb53e..f6070fb1 100644
--- a/core/src/main/java/tech/tablesaw/api/DateColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateColumn.java
@@ -244,7 +244,8 @@ public class DateColumn extends AbstractColumn<LocalDate> implements DateFilters
     public DateColumn append(final Column<LocalDate> column) {
         Preconditions.checkArgument(column.type() == this.type());
         DateColumn dateColumn = (DateColumn) column;
-        for (int i = 0; i < dateColumn.size(); i++) {
+        final int size = dateColumn.size();
+        for (int i = 0; i < size; i++) {
             appendInternal(dateColumn.getPackedDate(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
index 466fb915..5675b883 100644
--- a/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
@@ -437,7 +437,8 @@ public class DateTimeColumn extends AbstractColumn<LocalDateTime>
     public DateTimeColumn append(Column<LocalDateTime> column) {
         Preconditions.checkArgument(column.type() == this.type());
         DateTimeColumn dateTimeColumn = (DateTimeColumn) column;
-        for (int i = 0; i < dateTimeColumn.size(); i++) {
+        final int size = dateTimeColumn.size();
+        for (int i = 0; i < size; i++) {
             append(dateTimeColumn.get(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
index ad4879dd..4c1bc72a 100644
--- a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
@@ -294,7 +294,8 @@ public class DoubleColumn extends NumberColumn<Double> implements NumberFillers<
     public DoubleColumn append(final Column<Double> column) {
         Preconditions.checkArgument(column.type() == this.type());
         final DoubleColumn numberColumn = (DoubleColumn) column;
-        for (int i = 0; i < numberColumn.size(); i++) {
+        final int size = numberColumn.size();
+        for (int i = 0; i < size; i++) {
             append(numberColumn.getDouble(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/FloatColumn.java b/core/src/main/java/tech/tablesaw/api/FloatColumn.java
index de79e4f3..76842561 100644
--- a/core/src/main/java/tech/tablesaw/api/FloatColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/FloatColumn.java
@@ -215,7 +215,8 @@ public class FloatColumn extends NumberColumn<Float> {
     public FloatColumn append(final Column<Float> column) {
         Preconditions.checkArgument(column.type() == this.type());
         final FloatColumn numberColumn = (FloatColumn) column;
-        for (int i = 0; i < numberColumn.size(); i++) {
+        final int size = numberColumn.size();
+        for (int i = 0; i < size; i++) {
             append(numberColumn.getFloat(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/IntColumn.java b/core/src/main/java/tech/tablesaw/api/IntColumn.java
index 57bf7d71..ab8ead26 100644
--- a/core/src/main/java/tech/tablesaw/api/IntColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/IntColumn.java
@@ -234,7 +234,8 @@ public class IntColumn extends NumberColumn<Integer> implements CategoricalColum
     public IntColumn append(final Column<Integer> column) {
         Preconditions.checkArgument(column.type() == this.type());
         final IntColumn numberColumn = (IntColumn) column;
-        for (int i = 0; i < numberColumn.size(); i++) {
+        final int size = numberColumn.size();
+        for (int i = 0; i < size; i++) {
             append(numberColumn.getInt(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/LongColumn.java b/core/src/main/java/tech/tablesaw/api/LongColumn.java
index adb1fb4a..ff5d5e9a 100644
--- a/core/src/main/java/tech/tablesaw/api/LongColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/LongColumn.java
@@ -261,7 +261,8 @@ public class LongColumn extends NumberColumn<Long> implements CategoricalColumn<
     public LongColumn append(final Column<Long> column) {
         Preconditions.checkArgument(column.type() == this.type());
         final LongColumn numberColumn = (LongColumn) column;
-        for (int i = 0; i < numberColumn.size(); i++) {
+        final int size = numberColumn.size();
+        for (int i = 0; i < size; i++) {
             append(numberColumn.getLong(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/ShortColumn.java b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
index 3a759dde..06d7b1c2 100644
--- a/core/src/main/java/tech/tablesaw/api/ShortColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
@@ -199,9 +199,9 @@ public class ShortColumn extends NumberColumn<Short> implements CategoricalColum
 
     @Override
     public Object[] asObjectArray() {
-        final Integer[] output = new Integer[size()];
+        final Short[] output = new Short[size()];
         for (int i = 0; i < size(); i++) {
-            output[i] = getInt(i);
+            output[i] = getShort(i);
         }
         return output;
     }
@@ -225,7 +225,8 @@ public class ShortColumn extends NumberColumn<Short> implements CategoricalColum
     public ShortColumn append(final Column<Short> column) {
         Preconditions.checkArgument(column.type() == this.type());
         final ShortColumn numberColumn = (ShortColumn) column;
-        for (int i = 0; i < numberColumn.size(); i++) {
+        final int size = numberColumn.size();
+        for (int i = 0; i < size; i++) {
             append(numberColumn.getShort(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/api/StringColumn.java b/core/src/main/java/tech/tablesaw/api/StringColumn.java
index c1fcd455..e2f77632 100644
--- a/core/src/main/java/tech/tablesaw/api/StringColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/StringColumn.java
@@ -427,8 +427,9 @@ public class StringColumn extends AbstractColumn<String>
     public StringColumn append(Column<String> column) {
         Preconditions.checkArgument(column.type() == this.type());
         StringColumn source = (StringColumn) column;
-        for (String string : source) {
-            append(string);
+        final int size = source.size();
+        for (int i = 0; i < size; i++) {
+            append(source.getString(i));
         }
         return this;
     }
diff --git a/core/src/main/java/tech/tablesaw/api/TextColumn.java b/core/src/main/java/tech/tablesaw/api/TextColumn.java
index 9120fcbe..2abc62bb 100644
--- a/core/src/main/java/tech/tablesaw/api/TextColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TextColumn.java
@@ -386,8 +386,9 @@ public class TextColumn extends AbstractColumn<String>
     public TextColumn append(Column<String> column) {
         Preconditions.checkArgument(column.type() == this.type());
         TextColumn source = (TextColumn) column;
-        for (String string : source) {
-            append(string);
+        final int size = source.size();
+        for (int i = 0; i < size; i++) {
+            append(source.getString(i));
         }
         return this;
     }
diff --git a/core/src/main/java/tech/tablesaw/api/TimeColumn.java b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
index 6e0a2afd..3bd026c0 100644
--- a/core/src/main/java/tech/tablesaw/api/TimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
@@ -415,7 +415,8 @@ public class TimeColumn extends AbstractColumn<LocalTime>
     public TimeColumn append(Column<LocalTime> column) {
         Preconditions.checkArgument(column.type() == this.type());
         TimeColumn timeCol = (TimeColumn) column;
-        for (int i = 0; i < timeCol.size(); i++) {
+        final int size = timeCol.size();
+        for (int i = 0; i < size; i++) {
             appendInternal(timeCol.getIntInternal(i));
         }
         return this;
diff --git a/core/src/main/java/tech/tablesaw/columns/dates/DateMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/dates/DateMapFunctions.java
index 3a40a497..cbafc398 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/DateMapFunctions.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateMapFunctions.java
@@ -256,7 +256,8 @@ public interface DateMapFunctions extends Column<LocalDate> {
      */
     default IntColumn timeUntil(DateColumn end, ChronoUnit unit) {
 
-        IntColumn newColumn = IntColumn.create(name() + " - " + end.name());
+        IntColumn newColumn = IntColumn.create(name() + " - " + end.name() + "[" + unit.name() + "]");
+
         for (int r = 0; r < size(); r++) {
             int c1 = getIntInternal(r);
             int c2 = end.getIntInternal(r);
diff --git a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java
index adedc345..5f844a52 100644
--- a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeMapFunctions.java
@@ -61,7 +61,7 @@ public interface DateTimeMapFunctions extends Column<LocalDateTime> {
 
     default LongColumn difference(DateTimeColumn column2, ChronoUnit unit) {
 
-	    LongColumn newColumn = LongColumn.create(name() + " - " + column2.name());
+	    LongColumn newColumn = LongColumn.create(name() + " - " + column2.name() + "[" + unit.name() + "]");
 
         for (int r = 0; r < size(); r++) {
             if (this.isMissing(r) || column2.isMissing(r)) {
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java b/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java
index dc9063ec..932ae3b7 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/ByteDictionaryMap.java
@@ -178,8 +178,8 @@ public class ByteDictionaryMap implements DictionaryMap {
 
     @Override
     public Object[] asObjectArray() {
-        final String[] output = new String[values().size()];
-        for (int i = 0; i < values().size(); i++) {
+        final String[] output = new String[size()];
+        for (int i = 0; i < size(); i++) {
             output[i] = getValueForIndex(i);
         }
         return output;
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java b/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java
index 414f723c..65a2c0eb 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/IntDictionaryMap.java
@@ -181,8 +181,8 @@ public class IntDictionaryMap implements DictionaryMap {
 
     @Override
     public Object[] asObjectArray() {
-        final String[] output = new String[values().size()];
-        for (int i = 0; i < values().size(); i++) {
+        final String[] output = new String[size()];
+        for (int i = 0; i < size(); i++) {
             output[i] = getValueForIndex(i);
         }
         return output;
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java b/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java
index dbf68905..14582190 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/ShortDictionaryMap.java
@@ -186,8 +186,8 @@ public class ShortDictionaryMap implements DictionaryMap {
 
     @Override
     public Object[] asObjectArray() {
-        final String[] output = new String[values().size()];
-        for (int i = 0; i < values().size(); i++) {
+        final String[] output = new String[size()];
+        for (int i = 0; i < size(); i++) {
             output[i] = getValueForIndex(i);
         }
         return output;
diff --git a/core/src/main/java/tech/tablesaw/columns/times/TimeMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/times/TimeMapFunctions.java
index ccf66cef..daf5360d 100644
--- a/core/src/main/java/tech/tablesaw/columns/times/TimeMapFunctions.java
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeMapFunctions.java
@@ -62,7 +62,7 @@ public interface TimeMapFunctions extends Column<LocalTime> {
     TimeColumn lag(int n);
 
     default IntColumn difference(TimeColumn column2, ChronoUnit unit) {
-        IntColumn newColumn = IntColumn.create(name() + " - " + column2.name());
+        IntColumn newColumn = IntColumn.create(name() + " - " + column2.name() + "[" + unit.name() + "]");
 
         for (int r = 0; r < size(); r++) {
             int c1 = this.getIntInternal(r);


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
index b526c382..c08f1ac2 100644
--- a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
@@ -20,6 +20,7 @@ import tech.tablesaw.TestDataUtil;
 import tech.tablesaw.columns.strings.StringColumnFormatter;
 import tech.tablesaw.selection.Selection;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.Function;
 
@@ -52,6 +53,12 @@ TODO: fix
     }
 */
 
+    @Test
+    public void testAppendObj2() {
+        final StringColumn sc = StringColumn.create("sc", Arrays.asList("a", "b", "c", "a"));
+        assertArrayEquals(sc.asList().toArray(), sc.asObjectArray());
+    }
+
     @Test
     public void testAppendObj() {
         StringColumn column = StringColumn.create("testing");
diff --git a/core/src/test/java/tech/tablesaw/columns/ColumnAppendTest.java b/core/src/test/java/tech/tablesaw/columns/ColumnAppendTest.java
new file mode 100644
index 00000000..8f7a2690
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/columns/ColumnAppendTest.java
@@ -0,0 +1,131 @@
+package tech.tablesaw.columns;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import tech.tablesaw.api.*;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Theories.class)
+public class ColumnAppendTest {
+
+    private static class Scenario<T extends Column> {
+        public final T col1;
+        public final T col2;
+        public final List col1col2Appended;
+
+        public Scenario(T col) {
+            this(col, col);
+        }
+
+        public Scenario(T col1, T col2) {
+            this.col1 = col1;
+            this.col2 = col2;
+            this.col1col2Appended = Arrays.asList(ArrayUtils.addAll(this.col1.asObjectArray(), this.col2.asObjectArray()));
+        }
+    }
+
+    @DataPoints
+    public static Scenario[] scenarios() {
+        return new Scenario[]{
+                new Scenario<>(
+                        FloatColumn.create("floatCol1", new float[]{1f, 2f, 3f}),
+                        FloatColumn.create("floatCol2", new float[]{4f})
+                ),
+                new Scenario<>(
+                        FloatColumn.create("floatCol1", new float[]{5f})
+                ),
+                new Scenario<>(
+                        DoubleColumn.create("doubleCol1", new double[]{5d})
+                ),
+                new Scenario<>(
+                        DoubleColumn.create("doubleCol1", new double[]{1d, 2d, 3d}),
+                        DoubleColumn.create("doubleCol2", new double[]{4d})
+                ),
+                new Scenario<>(
+                        DoubleColumn.create("doubleCol1", new double[]{5d})
+                ),
+                new Scenario<>(
+                        ShortColumn.create("shortCol1", new short[]{1, 2, 3}),
+                        ShortColumn.create("shortCol2", new short[]{4})
+                ),
+                new Scenario<>(
+                        ShortColumn.create("shortCol1", new short[]{5})
+                ),
+                new Scenario<>(
+                        IntColumn.create("intCol1", new int[]{1, 2, 3}),
+                        IntColumn.create("intCol2", new int[]{4})
+                ),
+                new Scenario<>(
+                        IntColumn.create("intCol1", new int[]{5})
+                ),
+                new Scenario<>(
+                        LongColumn.create("longCol1", new long[]{1l, 2l, 3l}),
+                        LongColumn.create("longCol2", new long[]{4l})
+                ),
+                new Scenario<>(
+                        LongColumn.create("longCol1", new long[]{5l})
+                ),
+                new Scenario<>(
+                        BooleanColumn.create("boolCol1", new boolean[]{true}),
+                        BooleanColumn.create("boolCol2", new boolean[]{false})
+                ),
+                new Scenario<>(
+                        BooleanColumn.create("boolCol1", new boolean[]{true})
+                ),
+                new Scenario<>(
+                        DateColumn.create("dateCol1", new LocalDate[]{LocalDate.now()}),
+                        DateColumn.create("dateCol2", new LocalDate[]{LocalDate.now()})
+                ),
+                new Scenario<>(
+                        DateColumn.create("dateCol1", new LocalDate[]{LocalDate.now()})
+                ),
+                new Scenario<>(
+                        DateTimeColumn.create("dateTimeCol1", new LocalDateTime[]{LocalDateTime.now()}),
+                        DateTimeColumn.create("dateTimeCol2", new LocalDateTime[]{LocalDateTime.now()})
+                ),
+                new Scenario<>(
+                        DateTimeColumn.create("dateTimeCol1", new LocalDateTime[]{LocalDateTime.now()})
+                ),
+                new Scenario<>(
+                        StringColumn.create("stringCol1", new String[]{"foo"}),
+                        StringColumn.create("stringCol2", new String[]{"bar"})
+                ),
+                new Scenario<>(
+                        StringColumn.create("stringCol1", new String[]{"baz"})
+                ),
+                new Scenario<>(
+                        TextColumn.create("textCol1", new String[]{"foo"}),
+                        TextColumn.create("textCol2", new String[]{"bar"})
+                ),
+                new Scenario<>(
+                        TextColumn.create("textCol1", new String[]{"baz"})
+                ),
+                new Scenario<>(
+                        TimeColumn.create("timeCol1", new LocalTime[]{LocalTime.now()}),
+                        TimeColumn.create("timeCol2", new LocalTime[]{LocalTime.now()})
+                ),
+                new Scenario<>(
+                        TimeColumn.create("timeCol1", new LocalTime[]{LocalTime.now()})
+                )
+        };
+    }
+
+    @Theory
+    @SuppressWarnings("unchecked")
+    public void testColumnAppend(Scenario scenario) {
+        assertEquals(scenario.col1col2Appended, scenario.col1.append(scenario.col2).asList());
+    }
+
+}
+
+