package org.h2.expression.aggregate;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import org.h2.api.IntervalQualifier;
import org.h2.command.dml.SelectOrderBy;
import org.h2.engine.Database;
import org.h2.engine.Mode;
import org.h2.engine.Session;
import org.h2.engine.SysProperties;
import org.h2.expression.Expression;
import org.h2.expression.ExpressionColumn;
import org.h2.index.Cursor;
import org.h2.index.Index;
import org.h2.result.SearchRow;
import org.h2.result.SortOrder;
import org.h2.table.Column;
import org.h2.table.IndexColumn;
import org.h2.table.Table;
import org.h2.table.TableFilter;
import org.h2.util.DateTimeUtils;
import org.h2.util.IntervalUtils;
import org.h2.value.CompareMode;
import org.h2.value.Value;
import org.h2.value.ValueDate;
import org.h2.value.ValueDecimal;
import org.h2.value.ValueInterval;
import org.h2.value.ValueNull;
import org.h2.value.ValueTime;
import org.h2.value.ValueTimestamp;
import org.h2.value.ValueTimestampTimeZone;
final class Percentile {
static final BigDecimal HALF = BigDecimal.valueOf(0.5d);
private static boolean isNullsLast(Index index) {
IndexColumn ic = index.getIndexColumns()[0];
int sortType = ic.sortType;
return (sortType & SortOrder.NULLS_LAST) != 0
|| (sortType & SortOrder.NULLS_FIRST) == 0
&& (sortType & SortOrder.DESCENDING) != 0 ^ SysProperties.SORT_NULLS_HIGH;
}
static Index getColumnIndex(Expression on) {
if (on instanceof ExpressionColumn) {
ExpressionColumn col = (ExpressionColumn) on;
Column column = col.getColumn();
TableFilter filter = col.getTableFilter();
if (filter != null) {
Table table = filter.getTable();
ArrayList<Index> indexes = table.getIndexes();
Index result = null;
if (indexes != null) {
boolean nullable = column.isNullable();
for (int i = 1, size = indexes.size(); i < size; i++) {
Index index = indexes.get(i);
if (!index.canFindNext()) {
continue;
}
if (!index.isFirstColumn(column)) {
continue;
}
if (result == null || result.getColumns().length > index.getColumns().length
|| nullable && isNullsLast(result) && !isNullsLast(index)) {
result = index;
}
}
}
return result;
}
}
return null;
}
static Value getValue(Database database, Value[] array, int dataType, ArrayList<SelectOrderBy> orderByList,
BigDecimal percentile, boolean interpolate) {
final CompareMode compareMode = database.getCompareMode();
Arrays.sort(array, compareMode);
int count = array.length;
boolean reverseIndex = orderByList != null && (orderByList.get(0).sortType & SortOrder.DESCENDING) != 0;
BigDecimal fpRow = BigDecimal.valueOf(count - 1).multiply(percentile);
int rowIdx1 = fpRow.intValue();
BigDecimal factor = fpRow.subtract(BigDecimal.valueOf(rowIdx1));
int rowIdx2;
if (factor.signum() == 0) {
interpolate = false;
rowIdx2 = rowIdx1;
} else {
rowIdx2 = rowIdx1 + 1;
if (!interpolate) {
if (factor.compareTo(HALF) > 0) {
rowIdx1 = rowIdx2;
} else {
rowIdx2 = rowIdx1;
}
}
}
if (reverseIndex) {
rowIdx1 = count - 1 - rowIdx1;
rowIdx2 = count - 1 - rowIdx2;
}
Value v = array[rowIdx1];
if (!interpolate) {
return v.convertTo(dataType);
}
return interpolate(v, array[rowIdx2], factor, dataType, database.getMode(), compareMode);
}
static Value getFromIndex(Session session, Expression expression, int dataType,
ArrayList<SelectOrderBy> orderByList, BigDecimal percentile, boolean interpolate) {
Index index = getColumnIndex(expression);
long count = index.getRowCount(session);
if (count == 0) {
return ValueNull.INSTANCE;
}
Cursor cursor = index.find(session, null, null);
cursor.next();
int columnId = index.getColumns()[0].getColumnId();
ExpressionColumn expr = (ExpressionColumn) expression;
if (expr.getColumn().isNullable()) {
boolean hasNulls = false;
SearchRow row;
while (count > 0) {
row = cursor.getSearchRow();
if (row == null) {
return ValueNull.INSTANCE;
}
if (row.getValue(columnId) == ValueNull.INSTANCE) {
count--;
cursor.next();
hasNulls = true;
} else {
break;
}
}
if (count == 0) {
return ValueNull.INSTANCE;
}
if (!hasNulls && isNullsLast(index)) {
TableFilter tableFilter = expr.getTableFilter();
SearchRow check = tableFilter.getTable().getTemplateSimpleRow(true);
check.setValue(columnId, ValueNull.INSTANCE);
Cursor nullsCursor = index.find(session, check, check);
while (nullsCursor.next()) {
count--;
}
if (count <= 0) {
return ValueNull.INSTANCE;
}
}
}
boolean reverseIndex = (orderByList != null ? orderByList.get(0).sortType & SortOrder.DESCENDING : 0)
!= (index.getIndexColumns()[0].sortType & SortOrder.DESCENDING);
BigDecimal fpRow = BigDecimal.valueOf(count - 1).multiply(percentile);
long rowIdx1 = fpRow.longValue();
BigDecimal factor = fpRow.subtract(BigDecimal.valueOf(rowIdx1));
long rowIdx2;
if (factor.signum() == 0) {
interpolate = false;
rowIdx2 = rowIdx1;
} else {
rowIdx2 = rowIdx1 + 1;
if (!interpolate) {
if (factor.compareTo(HALF) > 0) {
rowIdx1 = rowIdx2;
} else {
rowIdx2 = rowIdx1;
}
}
}
long skip = reverseIndex ? count - 1 - rowIdx2 : rowIdx1;
for (int i = 0; i < skip; i++) {
cursor.next();
}
SearchRow row = cursor.getSearchRow();
if (row == null) {
return ValueNull.INSTANCE;
}
Value v = row.getValue(columnId);
if (v == ValueNull.INSTANCE) {
return v;
}
if (interpolate) {
cursor.next();
row = cursor.getSearchRow();
if (row == null) {
return v;
}
Value v2 = row.getValue(columnId);
if (v2 == ValueNull.INSTANCE) {
return v;
}
Database database = session.getDatabase();
if (reverseIndex) {
Value t = v;
v = v2;
v2 = t;
}
return interpolate(v, v2, factor, dataType, database.getMode(), database.getCompareMode());
}
return v.convertTo(dataType);
}
private static Value interpolate(Value v0, Value v1, BigDecimal factor, int dataType, Mode databaseMode,
CompareMode compareMode) {
if (v0.compareTo(v1, databaseMode, compareMode) == 0) {
return v0.convertTo(dataType);
}
switch (dataType) {
case Value.BYTE:
case Value.SHORT:
case Value.INT:
return ValueDecimal.get(
interpolateDecimal(BigDecimal.valueOf(v0.getInt()), BigDecimal.valueOf(v1.getInt()), factor));
case Value.LONG:
return ValueDecimal.get(
interpolateDecimal(BigDecimal.valueOf(v0.getLong()), BigDecimal.valueOf(v1.getLong()), factor));
case Value.DECIMAL:
return ValueDecimal.get(interpolateDecimal(v0.getBigDecimal(), v1.getBigDecimal(), factor));
case Value.FLOAT:
case Value.DOUBLE:
return ValueDecimal.get(
interpolateDecimal(
BigDecimal.valueOf(v0.getDouble()), BigDecimal.valueOf(v1.getDouble()), factor));
case Value.TIME: {
ValueTime t0 = (ValueTime) v0.convertTo(Value.TIME), t1 = (ValueTime) v1.convertTo(Value.TIME);
BigDecimal n0 = BigDecimal.valueOf(t0.getNanos());
BigDecimal n1 = BigDecimal.valueOf(t1.getNanos());
return ValueTime.fromNanos(interpolateDecimal(n0, n1, factor).longValue());
}
case Value.DATE: {
ValueDate d0 = (ValueDate) v0.convertTo(Value.DATE), d1 = (ValueDate) v1.convertTo(Value.DATE);
BigDecimal a0 = BigDecimal.valueOf(DateTimeUtils.absoluteDayFromDateValue(d0.getDateValue()));
BigDecimal a1 = BigDecimal.valueOf(DateTimeUtils.absoluteDayFromDateValue(d1.getDateValue()));
return ValueDate.fromDateValue(
DateTimeUtils.dateValueFromAbsoluteDay(interpolateDecimal(a0, a1, factor).longValue()));
}
case Value.TIMESTAMP: {
ValueTimestamp ts0 = (ValueTimestamp) v0.convertTo(Value.TIMESTAMP),
ts1 = (ValueTimestamp) v1.convertTo(Value.TIMESTAMP);
BigDecimal a0 = timestampToDecimal(ts0.getDateValue(), ts0.getTimeNanos());
BigDecimal a1 = timestampToDecimal(ts1.getDateValue(), ts1.getTimeNanos());
BigInteger[] dr = interpolateDecimal(a0, a1, factor).toBigInteger()
.divideAndRemainder(IntervalUtils.NANOS_PER_DAY_BI);
long absoluteDay = dr[0].longValue();
long timeNanos = dr[1].longValue();
if (timeNanos < 0) {
timeNanos += DateTimeUtils.NANOS_PER_DAY;
absoluteDay--;
}
return ValueTimestamp.fromDateValueAndNanos(
DateTimeUtils.dateValueFromAbsoluteDay(absoluteDay), timeNanos);
}
case Value.TIMESTAMP_TZ: {
ValueTimestampTimeZone ts0 = (ValueTimestampTimeZone) v0.convertTo(Value.TIMESTAMP_TZ),
ts1 = (ValueTimestampTimeZone) v1.convertTo(Value.TIMESTAMP_TZ);
BigDecimal a0 = timestampToDecimal(ts0.getDateValue(), ts0.getTimeNanos());
BigDecimal a1 = timestampToDecimal(ts1.getDateValue(), ts1.getTimeNanos());
BigDecimal offset = BigDecimal.valueOf(ts0.getTimeZoneOffsetMins())
.multiply(BigDecimal.ONE.subtract(factor))
.add(BigDecimal.valueOf(ts1.getTimeZoneOffsetMins()).multiply(factor));
short shortOffset = offset.shortValue();
BigDecimal shortOffsetBD = BigDecimal.valueOf(shortOffset);
BigDecimal bd = interpolateDecimal(a0, a1, factor);
if (offset.compareTo(shortOffsetBD) != 0) {
bd = bd.add(
offset.subtract(shortOffsetBD).multiply(BigDecimal.valueOf(DateTimeUtils.NANOS_PER_MINUTE)));
}
BigInteger[] dr = bd.toBigInteger().divideAndRemainder(IntervalUtils.NANOS_PER_DAY_BI);
long absoluteDay = dr[0].longValue();
long timeNanos = dr[1].longValue();
if (timeNanos < 0) {
timeNanos += DateTimeUtils.NANOS_PER_DAY;
absoluteDay--;
}
return ValueTimestampTimeZone.fromDateValueAndNanos(DateTimeUtils.dateValueFromAbsoluteDay(absoluteDay),
timeNanos, shortOffset);
}
case Value.INTERVAL_YEAR:
case Value.INTERVAL_MONTH:
case Value.INTERVAL_DAY:
case Value.INTERVAL_HOUR:
case Value.INTERVAL_MINUTE:
case Value.INTERVAL_SECOND:
case Value.INTERVAL_YEAR_TO_MONTH:
case Value.INTERVAL_DAY_TO_HOUR:
case Value.INTERVAL_DAY_TO_MINUTE:
case Value.INTERVAL_DAY_TO_SECOND:
case Value.INTERVAL_HOUR_TO_MINUTE:
case Value.INTERVAL_HOUR_TO_SECOND:
case Value.INTERVAL_MINUTE_TO_SECOND:
return IntervalUtils.intervalFromAbsolute(IntervalQualifier.valueOf(dataType - Value.INTERVAL_YEAR),
interpolateDecimal(new BigDecimal(IntervalUtils.intervalToAbsolute((ValueInterval) v0)),
new BigDecimal(IntervalUtils.intervalToAbsolute((ValueInterval) v1)), factor)
.toBigInteger());
default:
return (factor.compareTo(HALF) > 0 ? v1 : v0).convertTo(dataType);
}
}
private static BigDecimal timestampToDecimal(long dateValue, long timeNanos) {
return new BigDecimal(BigInteger.valueOf(DateTimeUtils.absoluteDayFromDateValue(dateValue))
.multiply(IntervalUtils.NANOS_PER_DAY_BI).add(BigInteger.valueOf(timeNanos)));
}
private static BigDecimal interpolateDecimal(BigDecimal d0, BigDecimal d1, BigDecimal factor) {
return d0.multiply(BigDecimal.ONE.subtract(factor)).add(d1.multiply(factor));
}
private Percentile() {
}
}