package com.linkedin.thirdeye.dataframe;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Series container for primitive long.
*/
public final class LongSeries extends TypedSeries<LongSeries> {
public static final long NULL = Long.MIN_VALUE;
public static final long DEFAULT = 0L;
public static final long MIN_VALUE = Long.MIN_VALUE + 1;
public static final long MAX_VALUE = Long.MAX_VALUE;
public static final LongFunction SUM = new LongSum();
public static final LongFunction PRODUCT = new LongProduct();
public static final LongFunction FIRST = new LongFirst();
public static final LongFunction LAST = new LongLast();
public static final LongFunction MIN = new LongMin();
public static final LongFunction MAX = new LongMax();
public static final class LongSum implements LongFunction {
@Override
public long apply(long[] values) {
if(values.length <= 0)
return NULL;
long result = 0;
for(long v : values)
result += v;
return result;
}
}
public static final class LongProduct implements LongFunction {
@Override
public long apply(long[] values) {
if(values.length <= 0)
return NULL;
long result = 1;
for(long v : values)
result *= v;
return result;
}
}
public static final class LongFirst implements LongFunction {
@Override
public long apply(long[] values) {
if(values.length <= 0)
return NULL;
return values[0];
}
}
public static final class LongLast implements LongFunction {
@Override
public long apply(long[] values) {
if(values.length <= 0)
return NULL;
return values[values.length - 1];
}
}
public static final class LongMin implements LongFunction {
@Override
public long apply(long[] values) {
if(values.length <= 0)
return NULL;
long min = values[0];
for(long v : values)
min = Math.min(min, v);
return min;
}
}
public static final class LongMax implements LongFunction {
@Override
public long apply(long[] values) {
if (values.length <= 0)
return NULL;
long max = values[0];
for (long v : values)
max = Math.max(max, v);
return max;
}
}
public static final class Builder extends Series.Builder {
final List<long[]> arrays = new ArrayList<>();
private Builder() {
// left blank
}
public Builder addValues(long... values) {
this.arrays.add(values);
return this;
}
public Builder addValues(long value) {
return this.addValues(new long[] { value });
}
public Builder addValues(Collection<Long> values) {
long[] newValues = new long[values.size()];
int i = 0;
for(Long v : values)
newValues[i++] = valueOf(v);
return this.addValues(newValues);
}
public Builder addValues(Long... values) {
return this.addValues(Arrays.asList(values));
}
public Builder addValues(Long value) {
return this.addValues(new long[] { valueOf(value) });
}
@Override
public Builder addSeries(Collection<Series> series) {
for(Series s : series)
this.addValues(s.getLongs().values);
return this;
}
public Builder fillValues(int count, long value) {
long[] values = new long[count];
Arrays.fill(values, value);
return this.addValues(values);
}
public Builder fillValues(int count, Long value) {
return this.fillValues(count, valueOf(value));
}
@Override
public LongSeries build() {
int totalSize = 0;
for(long[] array : this.arrays)
totalSize += array.length;
int offset = 0;
long[] values = new long[totalSize];
for(long[] array : this.arrays) {
System.arraycopy(array, 0, values, offset, array.length);
offset += array.length;
}
return new LongSeries(values);
}
}
public static Builder builder() {
return new Builder();
}
public static LongSeries buildFrom(long... values) {
return new LongSeries(values);
}
public static LongSeries empty() {
return new LongSeries();
}
public static LongSeries nulls(int size) {
return builder().fillValues(size, NULL).build();
}
public static LongSeries zeros(int size) {
return builder().fillValues(size, 0L).build();
}
public static LongSeries ones(int size) {
return builder().fillValues(size, 1L).build();
}
public static LongSeries fillValues(int size, long value) {
return builder().fillValues(size, value).build();
}
// CAUTION: The array is final, but values are inherently modifiable
final long[] values;
private LongSeries(long... values) {
this.values = values;
}
@Override
public Builder getBuilder() {
return new Builder();
}
@Override
public LongSeries getLongs() {
return this;
}
@Override
public double getDouble(int index) {
return getDouble(this.values[index]);
}
public static double getDouble(long value) {
if(LongSeries.isNull(value))
return DoubleSeries.NULL;
return (double) value;
}
@Override
public long getLong(int index) {
return getLong(this.values[index]);
}
public static long getLong(long value) {
return value;
}
@Override
public byte getBoolean(int index) {
return getBoolean(this.values[index]);
}
public static byte getBoolean(long value) {
if(LongSeries.isNull(value))
return BooleanSeries.NULL;
return BooleanSeries.valueOf(value != 0L);
}
@Override
public String getString(int index) {
return getString(this.values[index]);
}
public static String getString(long value) {
if(LongSeries.isNull(value))
return StringSeries.NULL;
return String.valueOf(value);
}
@Override
public boolean isNull(int index) {
return isNull(this.values[index]);
}
@Override
public int size() {
return this.values.length;
}
@Override
public SeriesType type() {
return SeriesType.LONG;
}
public long[] values() {
return this.values;
}
public long value() {
if(this.size() != 1)
throw new IllegalStateException("Series must contain exactly one element");
return this.values[0];
}
/**
* Returns the value of the first element in the series
*
* @throws IllegalStateException if the series is empty
* @return first element in the series
*/
public long first() {
assertNotEmpty(this.values);
return this.values[0];
}
/**
* Returns the value of the last element in the series
*
* @throws IllegalStateException if the series is empty
* @return last element in the series
*/
public long last() {
assertNotEmpty(this.values);
return this.values[this.values.length-1];
}
@Override
public LongSeries slice(int from, int to) {
return buildFrom(Arrays.copyOfRange(this.values, from, to));
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("LongSeries{");
for(long l : this.values) {
if(isNull(l)) {
builder.append("null");
} else {
builder.append(l);
}
builder.append(" ");
}
builder.append("}");
return builder.toString();
}
@Override
public String toString(int index) {
if(this.isNull(index))
return TOSTRING_NULL;
return String.valueOf(this.values[index]);
}
public SeriesGrouping groupByInterval(long interval) {
if(interval <= 0)
throw new IllegalArgumentException("interval must be greater than 0");
if(this.size() <= 0)
return new SeriesGrouping(this);
long start = this.min() / interval; // align with interval
long stop = this.max() / interval + 1;
List<Range> ranges = new ArrayList<>();
for(long i=start; i<stop; i++) {
ranges.add(new Range(i * interval, (i+1) * interval));
}
// turn ranges into buckets from original series
// TODO use nlogm solution to find matching range, e.g. ordered tree
long[] keys = new long[ranges.size()];
List<Bucket> buckets = new ArrayList<>();
int i = 0;
for(Range r : ranges) {
ArrayList<Integer> ind = new ArrayList<>();
for(int j=0; j<this.size(); j++) {
if(this.values[j] >= r.lower && this.values[j] < r.upper) {
ind.add(j);
}
}
int[] fromIndex = new int[ind.size()];
for(int j=0; j<ind.size(); j++) {
fromIndex[j] = ind.get(j);
}
buckets.add(new Bucket(fromIndex));
keys[i++] = r.lower;
}
return new SeriesGrouping(DataFrame.toSeries(keys), this, buckets);
}
public long min() {
return this.aggregate(MIN).value();
}
public long max() {
return this.aggregate(MAX).value();
}
public long sum() {
return this.aggregate(SUM).value();
}
public long product() {
return this.aggregate(PRODUCT).value();
}
public LongSeries add(Series other) {
return map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] + values[1];
}
}, this, other);
}
public LongSeries add(final long constant) {
if(isNull(constant))
return nulls(this.size());
return this.map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] + constant;
}
});
}
public LongSeries subtract(Series other) {
return map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] - values[1];
}
}, this, other);
}
public LongSeries subtract(final long constant) {
if(isNull(constant))
return nulls(this.size());
return this.map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] - constant;
}
});
}
public LongSeries multiply(Series other) {
return map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] * values[1];
}
}, this, other);
}
public LongSeries multiply(final long constant) {
if(isNull(constant))
return nulls(this.size());
return this.map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] * constant;
}
});
}
public LongSeries divide(Series other) {
return map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] / values[1];
}
}, this, other);
}
public LongSeries divide(final long constant) {
if(isNull(constant))
return nulls(this.size());
return this.map(new LongFunction() {
@Override
public long apply(long... values) {
return values[0] / constant;
}
});
}
public BooleanSeries eq(Series other) {
return map(new LongConditional() {
@Override
public boolean apply(long... values) {
return values[0] == values[1];
}
}, this, other);
}
public BooleanSeries eq(final long constant) {
if(isNull(constant))
return BooleanSeries.nulls(this.size());
return this.map(new LongConditional() {
@Override
public boolean apply(long... values) {
return values[0] == constant;
}
});
}
public LongSeries set(BooleanSeries where, long value) {
long[] values = new long[this.values.length];
for(int i=0; i<where.size(); i++) {
if(BooleanSeries.isTrue(where.getBoolean(i))) {
values[i] = value;
} else {
values[i] = this.values[i];
}
}
return buildFrom(values);
}
public int count(long value) {
int count = 0;
for(long v : this.values)
if(v == value)
count++;
return count;
}
public boolean contains(long value) {
return this.count(value) > 0;
}
public LongSeries replace(long find, long by) {
if(isNull(find))
return this.fillNull(by);
return this.set(this.eq(find), by);
}
@Override
public LongSeries filter(BooleanSeries filter) {
return this.set(filter.fillNull().not(), NULL);
}
@Override
public LongSeries fillNull() {
return this.fillNull(DEFAULT);
}
/**
* Return a copy of the series with all <b>null</b> values replaced by
* {@code value}.
*
* @param value replacement value for <b>null</b>
* @return series copy without nulls
*/
public LongSeries fillNull(long value) {
long[] values = Arrays.copyOf(this.values, this.values.length);
for(int i=0; i<values.length; i++) {
if(isNull(values[i])) {
values[i] = value;
}
}
return buildFrom(values);
}
@Override
LongSeries project(int[] fromIndex) {
long[] values = new long[fromIndex.length];
for(int i=0; i<fromIndex.length; i++) {
if(fromIndex[i] == -1) {
values[i] = NULL;
} else {
values[i] = this.values[fromIndex[i]];
}
}
return buildFrom(values);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LongSeries that = (LongSeries) o;
return Arrays.equals(this.values, that.values);
}
@Override
int compare(Series that, int indexThis, int indexThat) {
return Long.compare(this.values[indexThis], that.getLong(indexThat));
}
@Override
public int hashCode() {
return Arrays.hashCode(this.values);
}
/**
* @see DataFrame#map(Function, Series...)
*/
public static LongSeries map(LongFunction function, Series... series) {
if(series.length <= 0)
return empty();
DataFrame.assertSameLength(series);
// Note: code-specialization to help hot-spot vm
if(series.length == 1)
return mapUnrolled(function, series[0]);
if(series.length == 2)
return mapUnrolled(function, series[0], series[1]);
if(series.length == 3)
return mapUnrolled(function, series[0], series[1], series[2]);
long[] input = new long[series.length];
long[] output = new long[series[0].size()];
for(int i=0; i<series[0].size(); i++) {
output[i] = mapRow(function, series, input, i);
}
return buildFrom(output);
}
private static long mapRow(LongFunction function, Series[] series, long[] input, int row) {
for(int j=0; j<series.length; j++) {
long value = series[j].getLong(row);
if(isNull(value))
return NULL;
input[j] = value;
}
return function.apply(input);
}
private static LongSeries mapUnrolled(LongFunction function, Series a) {
long[] output = new long[a.size()];
for(int i=0; i<a.size(); i++) {
if(a.isNull(i)) {
output[i] = NULL;
} else {
output[i] = function.apply(a.getLong(i));
}
}
return buildFrom(output);
}
private static LongSeries mapUnrolled(LongFunction function, Series a, Series b) {
long[] output = new long[a.size()];
for(int i=0; i<a.size(); i++) {
if(a.isNull(i) || b.isNull(i)) {
output[i] = NULL;
} else {
output[i] = function.apply(a.getLong(i), b.getLong(i));
}
}
return buildFrom(output);
}
private static LongSeries mapUnrolled(LongFunction function, Series a, Series b, Series c) {
long[] output = new long[a.size()];
for(int i=0; i<a.size(); i++) {
if(a.isNull(i) || b.isNull(i) || c.isNull(i)) {
output[i] = NULL;
} else {
output[i] = function.apply(a.getLong(i), b.getLong(i), c.getLong(i));
}
}
return buildFrom(output);
}
/**
* @see DataFrame#map(Function, Series...)
*/
public static BooleanSeries map(LongConditional function, Series... series) {
if(series.length <= 0)
return BooleanSeries.empty();
DataFrame.assertSameLength(series);
long[] input = new long[series.length];
byte[] output = new byte[series[0].size()];
for(int i=0; i<series[0].size(); i++) {
output[i] = mapRow(function, series, input, i);
}
return BooleanSeries.buildFrom(output);
}
private static byte mapRow(LongConditional function, Series[] series, long[] input, int row) {
for(int j=0; j<series.length; j++) {
long value = series[j].getLong(row);
if(isNull(value))
return BooleanSeries.NULL;
input[j] = value;
}
return BooleanSeries.valueOf(function.apply(input));
}
/**
* @see Series#aggregate(Function)
*/
public static LongSeries aggregate(LongFunction function, Series series) {
if(series.hasNull())
return buildFrom(NULL);
return buildFrom(function.apply(series.getLongs().values));
}
/**
* @see Series#aggregate(Function)
*/
public static BooleanSeries aggregate(LongConditional function, Series series) {
if(series.hasNull())
return BooleanSeries.buildFrom(BooleanSeries.NULL);
return BooleanSeries.builder().addBooleanValues(function.apply(series.getLongs().values)).build();
}
public static long valueOf(Long value) {
if(value == null)
return NULL;
return value;
}
public static boolean isNull(long value) {
return value == NULL;
}
private static long[] assertNotEmpty(long[] values) {
if(values.length <= 0)
throw new IllegalStateException("Must contain at least one value");
return values;
}
static class Range {
final long lower;
final long upper; // exclusive
Range(long lower, long upper) {
this.lower = lower;
this.upper = upper;
}
}
@Override
public LongSeries shift(int offset) {
long[] values = new long[this.values.length];
if(offset >= 0) {
Arrays.fill(values, 0, Math.min(offset, values.length), NULL);
System.arraycopy(this.values, 0, values, Math.min(offset, values.length), Math.max(values.length - offset, 0));
} else {
System.arraycopy(this.values, Math.min(-offset, values.length), values, 0, Math.max(values.length + offset, 0));
Arrays.fill(values, Math.max(values.length + offset, 0), values.length, NULL);
}
return buildFrom(values);
}
@Override
public LongSeries sorted() {
long[] values = Arrays.copyOf(this.values, this.values.length);
Arrays.sort(values);
return buildFrom(values);
}
@Override
int[] sortedIndex() {
List<LongSortTuple> tuples = new ArrayList<>();
for (int i = 0; i < this.values.length; i++) {
tuples.add(new LongSortTuple(this.values[i], i));
}
Collections.sort(tuples, new Comparator<LongSortTuple>() {
@Override
public int compare(LongSortTuple a, LongSortTuple b) {
return Long.compare(a.value, b.value);
}
});
int[] fromIndex = new int[tuples.size()];
for (int i = 0; i < tuples.size(); i++) {
fromIndex[i] = tuples.get(i).index;
}
return fromIndex;
}
static final class LongSortTuple {
final long value;
final int index;
LongSortTuple(long value, int index) {
this.value = value;
this.index = index;
}
}
}