CE-881 - Update aggregates to include dates, plus product, variance, and standard deviation

This commit is contained in:
2024-04-01 08:54:32 -05:00
parent 5384eb9927
commit 782a07b176
9 changed files with 828 additions and 35 deletions

View File

@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -73,7 +75,9 @@ import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface; import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface;
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates; import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.InstantAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates; import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.LocalDateAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.LongAggregates; import com.kingsrook.qqq.backend.core.utils.aggregates.LongAggregates;
@ -103,11 +107,11 @@ public class GenerateReportAction
// Aggregates: (count:47;sum:10,000;max:2,000;min:15) // // Aggregates: (count:47;sum:10,000;max:2,000;min:15) //
// salesSummaryReport > [(state:MO),(city:St.Louis)] > salePrice > (count:47;sum:10,000;max:2,000;min:15) // // salesSummaryReport > [(state:MO),(city:St.Louis)] > salePrice > (count:47;sum:10,000;max:2,000;min:15) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> summaryAggregates = new HashMap<>(); Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> summaryAggregates = new HashMap<>();
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> varianceAggregates = new HashMap<>(); Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> varianceAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>(); Map<String, AggregatesInterface<?, ?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>(); Map<String, AggregatesInterface<?, ?>> varianceTotalAggregates = new HashMap<>();
private ExportStreamerInterface reportStreamer; private ExportStreamerInterface reportStreamer;
private List<QReportDataSource> dataSources; private List<QReportDataSource> dataSources;
@ -546,9 +550,9 @@ public class GenerateReportAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> aggregatesMap) private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> aggregatesMap)
{ {
Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>()); Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
for(QRecord record : records) for(QRecord record : records)
{ {
@ -584,9 +588,9 @@ public class GenerateReportAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates, SummaryKey key) private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key)
{ {
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>()); Map<String, AggregatesInterface<?, ?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
addRecordToAggregatesMap(table, record, keyAggregates); addRecordToAggregatesMap(table, record, keyAggregates);
} }
@ -595,29 +599,45 @@ public class GenerateReportAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?>> aggregatesMap) private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> aggregatesMap)
{ {
for(QFieldMetaData field : table.getFields().values()) for(QFieldMetaData field : table.getFields().values())
{ {
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
continue;
}
if(field.getType().equals(QFieldType.INTEGER)) if(field.getType().equals(QFieldType.INTEGER))
{ {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates()); AggregatesInterface<Integer, ?> fieldAggregates = (AggregatesInterface<Integer, ?>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(field.getName())); fieldAggregates.add(record.getValueInteger(field.getName()));
} }
else if(field.getType().equals(QFieldType.LONG)) else if(field.getType().equals(QFieldType.LONG))
{ {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
AggregatesInterface<Long> fieldAggregates = (AggregatesInterface<Long>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LongAggregates()); AggregatesInterface<Long, ?> fieldAggregates = (AggregatesInterface<Long, ?>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LongAggregates());
fieldAggregates.add(record.getValueLong(field.getName())); fieldAggregates.add(record.getValueLong(field.getName()));
} }
else if(field.getType().equals(QFieldType.DECIMAL)) else if(field.getType().equals(QFieldType.DECIMAL))
{ {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates()); AggregatesInterface<BigDecimal, ?> fieldAggregates = (AggregatesInterface<BigDecimal, ?>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
fieldAggregates.add(record.getValueBigDecimal(field.getName())); fieldAggregates.add(record.getValueBigDecimal(field.getName()));
} }
// todo - more types (dates, at least?) else if(field.getType().equals(QFieldType.DATE_TIME))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Instant, ?> fieldAggregates = (AggregatesInterface<Instant, ?>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new InstantAggregates());
fieldAggregates.add(record.getValueInstant(field.getName()));
}
else if(field.getType().equals(QFieldType.DATE))
{
@SuppressWarnings("unchecked")
AggregatesInterface<LocalDate, ?> fieldAggregates = (AggregatesInterface<LocalDate, ?>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LocalDateAggregates());
fieldAggregates.add(record.getValueLocalDate(field.getName()));
}
} }
} }
@ -735,11 +755,11 @@ public class GenerateReportAction
// create summary rows // // create summary rows //
///////////////////////// /////////////////////////
List<QRecord> summaryRows = new ArrayList<>(); List<QRecord> summaryRows = new ArrayList<>();
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet()) for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?, ?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
{ {
SummaryKey summaryKey = entry.getKey(); SummaryKey summaryKey = entry.getKey();
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue(); Map<String, AggregatesInterface<?, ?>> fieldAggregates = entry.getValue();
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates); Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
variableInterpreter.addValueMap("pivot", summaryValues); variableInterpreter.addValueMap("pivot", summaryValues);
variableInterpreter.addValueMap("summary", summaryValues); variableInterpreter.addValueMap("summary", summaryValues);
@ -748,9 +768,9 @@ public class GenerateReportAction
if(!varianceAggregates.isEmpty()) if(!varianceAggregates.isEmpty())
{ {
Map<SummaryKey, Map<String, AggregatesInterface<?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap()); Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap()); Map<String, AggregatesInterface<?, ?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap); Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
variableInterpreter.addValueMap("variancePivot", varianceValues); variableInterpreter.addValueMap("variancePivot", varianceValues);
variableInterpreter.addValueMap("variance", varianceValues); variableInterpreter.addValueMap("variance", varianceValues);
} }
@ -931,18 +951,24 @@ public class GenerateReportAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates) private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?, ?>> fieldAggregates)
{ {
Map<String, Serializable> summaryValuesForInterpreter = new HashMap<>(); Map<String, Serializable> summaryValuesForInterpreter = new HashMap<>();
for(Map.Entry<String, AggregatesInterface<?>> subEntry : fieldAggregates.entrySet()) for(Map.Entry<String, AggregatesInterface<?, ?>> subEntry : fieldAggregates.entrySet())
{ {
String fieldName = subEntry.getKey(); String fieldName = subEntry.getKey();
AggregatesInterface<?> aggregates = subEntry.getValue(); AggregatesInterface<?, ?> aggregates = subEntry.getValue();
summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum()); summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount()); summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
summaryValuesForInterpreter.put("count_nums." + fieldName, aggregates.getCount());
summaryValuesForInterpreter.put("min." + fieldName, aggregates.getMin()); summaryValuesForInterpreter.put("min." + fieldName, aggregates.getMin());
summaryValuesForInterpreter.put("max." + fieldName, aggregates.getMax()); summaryValuesForInterpreter.put("max." + fieldName, aggregates.getMax());
summaryValuesForInterpreter.put("average." + fieldName, aggregates.getAverage()); summaryValuesForInterpreter.put("average." + fieldName, aggregates.getAverage());
summaryValuesForInterpreter.put("product." + fieldName, aggregates.getProduct());
summaryValuesForInterpreter.put("var." + fieldName, aggregates.getVariance());
summaryValuesForInterpreter.put("varp." + fieldName, aggregates.getVarP());
summaryValuesForInterpreter.put("std_dev." + fieldName, aggregates.getStandardDeviation());
summaryValuesForInterpreter.put("std_devp." + fieldName, aggregates.getStdDevP());
} }
return summaryValuesForInterpreter; return summaryValuesForInterpreter;
} }

View File

@ -29,8 +29,12 @@ import java.math.BigDecimal;
/******************************************************************************* /*******************************************************************************
** Classes that support doing data aggregations (e.g., count, sum, min, max, average). ** Classes that support doing data aggregations (e.g., count, sum, min, max, average).
** Sub-classes should supply the type parameter. ** Sub-classes should supply the type parameter.
**
** The AVG_T parameter describes the type used for the average getAverage method
** which, e.g, for date types, might be a date, vs. numbers, they'd probably be
** BigDecimal.
*******************************************************************************/ *******************************************************************************/
public interface AggregatesInterface<T extends Serializable> public interface AggregatesInterface<T extends Serializable, AVG_T extends Serializable>
{ {
/******************************************************************************* /*******************************************************************************
** **
@ -60,5 +64,51 @@ public interface AggregatesInterface<T extends Serializable>
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
BigDecimal getAverage(); AVG_T getAverage();
/*******************************************************************************
**
*******************************************************************************/
default BigDecimal getProduct()
{
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
default BigDecimal getVariance()
{
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
default BigDecimal getVarP()
{
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
default BigDecimal getStandardDeviation()
{
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
default BigDecimal getStdDevP()
{
return (null);
}
} }

View File

@ -28,13 +28,16 @@ import java.math.BigDecimal;
/******************************************************************************* /*******************************************************************************
** BigDecimal version of data aggregator ** BigDecimal version of data aggregator
*******************************************************************************/ *******************************************************************************/
public class BigDecimalAggregates implements AggregatesInterface<BigDecimal> public class BigDecimalAggregates implements AggregatesInterface<BigDecimal, BigDecimal>
{ {
private int count = 0; private int count = 0;
// private Integer countDistinct; // private Integer countDistinct;
private BigDecimal sum; private BigDecimal sum;
private BigDecimal min; private BigDecimal min;
private BigDecimal max; private BigDecimal max;
private BigDecimal product;
private VarianceCalculator varianceCalculator = new VarianceCalculator();
@ -59,6 +62,15 @@ public class BigDecimalAggregates implements AggregatesInterface<BigDecimal>
sum = sum.add(input); sum = sum.add(input);
} }
if(product == null)
{
product = input;
}
else
{
product = product.multiply(input);
}
if(min == null || input.compareTo(min) < 0) if(min == null || input.compareTo(min) < 0)
{ {
min = input; min = input;
@ -68,6 +80,52 @@ public class BigDecimalAggregates implements AggregatesInterface<BigDecimal>
{ {
max = input; max = input;
} }
varianceCalculator.updateVariance(input);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getVariance()
{
return (varianceCalculator.getVariance());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getVarP()
{
return (varianceCalculator.getVarP());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getStandardDeviation()
{
return (varianceCalculator.getStandardDeviation());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getStdDevP()
{
return (varianceCalculator.getStdDevP());
} }
@ -116,6 +174,18 @@ public class BigDecimalAggregates implements AggregatesInterface<BigDecimal>
/*******************************************************************************
** Getter for product
**
*******************************************************************************/
@Override
public BigDecimal getProduct()
{
return product;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,136 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils.aggregates;
import java.math.BigInteger;
import java.time.Instant;
/*******************************************************************************
** Instant version of data aggregator
*******************************************************************************/
public class InstantAggregates implements AggregatesInterface<Instant, Instant>
{
private int count = 0;
// private Integer countDistinct;
private BigInteger sumMillis = BigInteger.ZERO;
private Instant min;
private Instant max;
/*******************************************************************************
** Add a new value to this aggregate set
*******************************************************************************/
public void add(Instant input)
{
if(input == null)
{
return;
}
count++;
sumMillis = sumMillis.add(new BigInteger(String.valueOf(input.toEpochMilli())));
if(min == null || input.compareTo(min) < 0)
{
min = input;
}
if(max == null || input.compareTo(max) > 0)
{
max = input;
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int getCount()
{
return (count);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant getSum()
{
//////////////////////////////////////////
// sum of date-times doesn't make sense //
//////////////////////////////////////////
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant getMin()
{
return (min);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant getMax()
{
return (max);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant getAverage()
{
if(this.count > 0)
{
BigInteger averageMillis = this.sumMillis.divide(new BigInteger(String.valueOf(count)));
if(averageMillis.compareTo(new BigInteger(String.valueOf(Long.MAX_VALUE))) < 0)
{
return (Instant.ofEpochMilli(averageMillis.longValue()));
}
}
return (null);
}
}

View File

@ -28,13 +28,16 @@ import java.math.BigDecimal;
/******************************************************************************* /*******************************************************************************
** Integer version of data aggregator ** Integer version of data aggregator
*******************************************************************************/ *******************************************************************************/
public class IntegerAggregates implements AggregatesInterface<Integer> public class IntegerAggregates implements AggregatesInterface<Integer, BigDecimal>
{ {
private int count = 0; private int count = 0;
// private Integer countDistinct; // private Integer countDistinct;
private Integer sum; private Integer sum;
private Integer min; private Integer min;
private Integer max; private Integer max;
private BigDecimal product;
private VarianceCalculator varianceCalculator = new VarianceCalculator();
@ -48,6 +51,8 @@ public class IntegerAggregates implements AggregatesInterface<Integer>
return; return;
} }
BigDecimal inputBD = new BigDecimal(input);
count++; count++;
if(sum == null) if(sum == null)
@ -59,6 +64,15 @@ public class IntegerAggregates implements AggregatesInterface<Integer>
sum = sum + input; sum = sum + input;
} }
if(product == null)
{
product = inputBD;
}
else
{
product = product.multiply(inputBD);
}
if(min == null || input < min) if(min == null || input < min)
{ {
min = input; min = input;
@ -68,6 +82,52 @@ public class IntegerAggregates implements AggregatesInterface<Integer>
{ {
max = input; max = input;
} }
varianceCalculator.updateVariance(inputBD);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getVariance()
{
return (varianceCalculator.getVariance());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getVarP()
{
return (varianceCalculator.getVarP());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getStandardDeviation()
{
return (varianceCalculator.getStandardDeviation());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getStdDevP()
{
return (varianceCalculator.getStdDevP());
} }
@ -116,6 +176,18 @@ public class IntegerAggregates implements AggregatesInterface<Integer>
/*******************************************************************************
** Getter for product
**
*******************************************************************************/
@Override
public BigDecimal getProduct()
{
return product;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,136 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils.aggregates;
import java.math.BigInteger;
import java.time.LocalDate;
/*******************************************************************************
** LocalDate version of data aggregator
*******************************************************************************/
public class LocalDateAggregates implements AggregatesInterface<LocalDate, LocalDate>
{
private int count = 0;
// private Integer countDistinct;
private BigInteger sumMillis = BigInteger.ZERO;
private LocalDate min;
private LocalDate max;
/*******************************************************************************
** Add a new value to this aggregate set
*******************************************************************************/
public void add(LocalDate input)
{
if(input == null)
{
return;
}
count++;
sumMillis = sumMillis.add(new BigInteger(String.valueOf(input.toEpochDay())));
if(min == null || input.compareTo(min) < 0)
{
min = input;
}
if(max == null || input.compareTo(max) > 0)
{
max = input;
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int getCount()
{
return (count);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public LocalDate getSum()
{
//////////////////////////////////////////
// sum of date-times doesn't make sense //
//////////////////////////////////////////
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public LocalDate getMin()
{
return (min);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public LocalDate getMax()
{
return (max);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public LocalDate getAverage()
{
if(this.count > 0)
{
BigInteger averageEpochDay = this.sumMillis.divide(new BigInteger(String.valueOf(count)));
if(averageEpochDay.compareTo(new BigInteger(String.valueOf(Long.MAX_VALUE))) < 0)
{
return (LocalDate.ofEpochDay(averageEpochDay.longValue()));
}
}
return (null);
}
}

View File

@ -28,13 +28,16 @@ import java.math.BigDecimal;
/******************************************************************************* /*******************************************************************************
** Long version of data aggregator ** Long version of data aggregator
*******************************************************************************/ *******************************************************************************/
public class LongAggregates implements AggregatesInterface<Long> public class LongAggregates implements AggregatesInterface<Long, BigDecimal>
{ {
private int count = 0; private int count = 0;
// private Long countDistinct; // private Long countDistinct;
private Long sum; private Long sum;
private Long min; private Long min;
private Long max; private Long max;
private BigDecimal product;
private VarianceCalculator varianceCalculator = new VarianceCalculator();
@ -48,6 +51,8 @@ public class LongAggregates implements AggregatesInterface<Long>
return; return;
} }
BigDecimal inputBD = new BigDecimal(input);
count++; count++;
if(sum == null) if(sum == null)
@ -59,6 +64,15 @@ public class LongAggregates implements AggregatesInterface<Long>
sum = sum + input; sum = sum + input;
} }
if(product == null)
{
product = inputBD;
}
else
{
product = product.multiply(inputBD);
}
if(min == null || input < min) if(min == null || input < min)
{ {
min = input; min = input;
@ -68,6 +82,52 @@ public class LongAggregates implements AggregatesInterface<Long>
{ {
max = input; max = input;
} }
varianceCalculator.updateVariance(inputBD);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getVariance()
{
return (varianceCalculator.getVariance());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getVarP()
{
return (varianceCalculator.getVarP());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getStandardDeviation()
{
return (varianceCalculator.getStandardDeviation());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public BigDecimal getStdDevP()
{
return (varianceCalculator.getStdDevP());
} }
@ -116,6 +176,18 @@ public class LongAggregates implements AggregatesInterface<Long>
/*******************************************************************************
** Getter for product
**
*******************************************************************************/
@Override
public BigDecimal getProduct()
{
return product;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,117 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils.aggregates;
import java.math.BigDecimal;
import java.math.RoundingMode;
/*******************************************************************************
** see https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
**
*******************************************************************************/
public class VarianceCalculator
{
private int n;
private BigDecimal runningMean = BigDecimal.ZERO;
private BigDecimal m2 = BigDecimal.ZERO;
public static int scaleForVarianceCalculations = 4;
/*******************************************************************************
**
*******************************************************************************/
public void updateVariance(BigDecimal newInput)
{
n++;
BigDecimal delta = newInput.subtract(runningMean);
runningMean = runningMean.add(delta.divide(new BigDecimal(n), scaleForVarianceCalculations, RoundingMode.HALF_UP));
BigDecimal delta2 = newInput.subtract(runningMean);
m2 = m2.add(delta.multiply(delta2));
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getVariance()
{
if(n < 2)
{
return (null);
}
return m2.divide(new BigDecimal(n - 1), scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getVarP()
{
if(n < 2)
{
return (null);
}
return m2.divide(new BigDecimal(n), scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getStandardDeviation()
{
BigDecimal variance = getVariance();
if(variance == null)
{
return (null);
}
return BigDecimal.valueOf(Math.sqrt(variance.doubleValue())).setScale(scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getStdDevP()
{
BigDecimal varP = getVarP();
if(varP == null)
{
return (null);
}
return BigDecimal.valueOf(Math.sqrt(varP.doubleValue())).setScale(scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
}

View File

@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.utils.aggregates;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Month;
import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.BaseTest;
import org.assertj.core.data.Offset; import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -78,6 +81,12 @@ class AggregatesTest extends BaseTest
assertEquals(15, aggregates.getMax()); assertEquals(15, aggregates.getMax());
assertEquals(30, aggregates.getSum()); assertEquals(30, aggregates.getSum());
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10"), Offset.offset(BigDecimal.ZERO)); assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10"), Offset.offset(BigDecimal.ZERO));
assertEquals(new BigDecimal("750"), aggregates.getProduct());
assertEquals(new BigDecimal("25.0000"), aggregates.getVariance());
assertEquals(new BigDecimal("5.0000"), aggregates.getStandardDeviation());
assertThat(aggregates.getVarP()).isCloseTo(new BigDecimal("16.6667"), Offset.offset(new BigDecimal(".0001")));
assertThat(aggregates.getStdDevP()).isCloseTo(new BigDecimal("4.0824"), Offset.offset(new BigDecimal(".0001")));
} }
@ -89,6 +98,7 @@ class AggregatesTest extends BaseTest
void testBigDecimal() void testBigDecimal()
{ {
BigDecimalAggregates aggregates = new BigDecimalAggregates(); BigDecimalAggregates aggregates = new BigDecimalAggregates();
aggregates.add(null);
assertEquals(0, aggregates.getCount()); assertEquals(0, aggregates.getCount());
assertNull(aggregates.getMin()); assertNull(aggregates.getMin());
@ -114,13 +124,117 @@ class AggregatesTest extends BaseTest
BigDecimal bd148 = new BigDecimal("14.8"); BigDecimal bd148 = new BigDecimal("14.8");
aggregates.add(bd148); aggregates.add(bd148);
aggregates.add(null);
assertEquals(3, aggregates.getCount()); assertEquals(3, aggregates.getCount());
assertEquals(bd51, aggregates.getMin()); assertEquals(bd51, aggregates.getMin());
assertEquals(bd148, aggregates.getMax()); assertEquals(bd148, aggregates.getMax());
assertEquals(new BigDecimal("30.0"), aggregates.getSum()); assertEquals(new BigDecimal("30.0"), aggregates.getSum());
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10.0"), Offset.offset(BigDecimal.ZERO)); assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10.0"), Offset.offset(BigDecimal.ZERO));
assertEquals(new BigDecimal("762.348"), aggregates.getProduct());
assertEquals(new BigDecimal("23.5300"), aggregates.getVariance());
assertEquals(new BigDecimal("4.8508"), aggregates.getStandardDeviation());
assertThat(aggregates.getVarP()).isCloseTo(new BigDecimal("15.6867"), Offset.offset(new BigDecimal(".0001")));
assertThat(aggregates.getStdDevP()).isCloseTo(new BigDecimal("3.9606"), Offset.offset(new BigDecimal(".0001")));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInstant()
{
InstantAggregates aggregates = new InstantAggregates();
assertEquals(0, aggregates.getCount());
assertNull(aggregates.getMin());
assertNull(aggregates.getMax());
assertNull(aggregates.getSum());
assertNull(aggregates.getAverage());
Instant i1970 = Instant.parse("1970-01-01T00:00:00Z");
aggregates.add(i1970);
assertEquals(1, aggregates.getCount());
assertEquals(i1970, aggregates.getMin());
assertEquals(i1970, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(i1970, aggregates.getAverage());
Instant i1980 = Instant.parse("1980-01-01T00:00:00Z");
aggregates.add(i1980);
assertEquals(2, aggregates.getCount());
assertEquals(i1970, aggregates.getMin());
assertEquals(i1980, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(Instant.parse("1975-01-01T00:00:00Z"), aggregates.getAverage());
Instant i1990 = Instant.parse("1990-01-01T00:00:00Z");
aggregates.add(i1990);
assertEquals(3, aggregates.getCount());
assertEquals(i1970, aggregates.getMin());
assertEquals(i1990, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(Instant.parse("1980-01-01T08:00:00Z"), aggregates.getAverage()); // a leap day throws this off by 8 hours :)
/////////////////////////////////////////////////////////////////////
// assert we gracefully return null for these ops we don't support //
/////////////////////////////////////////////////////////////////////
assertNull(aggregates.getProduct());
assertNull(aggregates.getVariance());
assertNull(aggregates.getStandardDeviation());
assertNull(aggregates.getVarP());
assertNull(aggregates.getStdDevP());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testLocalDate()
{
LocalDateAggregates aggregates = new LocalDateAggregates();
assertEquals(0, aggregates.getCount());
assertNull(aggregates.getMin());
assertNull(aggregates.getMax());
assertNull(aggregates.getSum());
assertNull(aggregates.getAverage());
LocalDate ld1970 = LocalDate.of(1970, Month.JANUARY, 1);
aggregates.add(ld1970);
assertEquals(1, aggregates.getCount());
assertEquals(ld1970, aggregates.getMin());
assertEquals(ld1970, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(ld1970, aggregates.getAverage());
LocalDate ld1980 = LocalDate.of(1980, Month.JANUARY, 1);
aggregates.add(ld1980);
assertEquals(2, aggregates.getCount());
assertEquals(ld1970, aggregates.getMin());
assertEquals(ld1980, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(LocalDate.of(1975, Month.JANUARY, 1), aggregates.getAverage());
LocalDate ld1990 = LocalDate.of(1990, Month.JANUARY, 1);
aggregates.add(ld1990);
assertEquals(3, aggregates.getCount());
assertEquals(ld1970, aggregates.getMin());
assertEquals(ld1990, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(ld1980, aggregates.getAverage());
/////////////////////////////////////////////////////////////////////
// assert we gracefully return null for these ops we don't support //
/////////////////////////////////////////////////////////////////////
assertNull(aggregates.getProduct());
assertNull(aggregates.getVariance());
assertNull(aggregates.getStandardDeviation());
assertNull(aggregates.getVarP());
assertNull(aggregates.getStdDevP());
} }
} }