mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-10 13:21:53 +01:00
Median action in persistence extensions (#4345)
* median persistence extension Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
parent
b8e0f94cb0
commit
b63fa473b3
@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import javax.measure.Unit;
|
||||
@ -43,6 +44,7 @@ import org.openhab.core.persistence.QueryablePersistenceService;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.TimeSeries;
|
||||
import org.openhab.core.types.TypeParser;
|
||||
import org.openhab.core.util.Statistics;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
@ -62,7 +64,8 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Mark Herwege - Changed return types to State for some interval methods to also return unit
|
||||
* @author Mark Herwege - Extended for future dates
|
||||
* @author Mark Herwege - lastChange and nextChange methods
|
||||
* @author mark Herwege - handle persisted GroupItem with QuantityType
|
||||
* @author Mark Herwege - handle persisted GroupItem with QuantityType
|
||||
* @author Mark Herwege - add median methods
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
@NonNullByDefault
|
||||
@ -986,10 +989,7 @@ public class PersistenceExtensions {
|
||||
Iterator<HistoricItem> it = result.iterator();
|
||||
HistoricItem maximumHistoricItem = null;
|
||||
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = (baseItem instanceof NumberItem numberItem) ? numberItem.getUnit() : null;
|
||||
|
||||
DecimalType maximum = null;
|
||||
@ -1117,10 +1117,7 @@ public class PersistenceExtensions {
|
||||
Iterator<HistoricItem> it = result.iterator();
|
||||
HistoricItem minimumHistoricItem = null;
|
||||
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = (baseItem instanceof NumberItem numberItem) ? numberItem.getUnit() : null;
|
||||
|
||||
DecimalType minimum = null;
|
||||
@ -1249,10 +1246,7 @@ public class PersistenceExtensions {
|
||||
BigDecimal average = dt != null ? dt.toBigDecimal() : BigDecimal.ZERO, sum = BigDecimal.ZERO;
|
||||
int count = 0;
|
||||
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = baseItem instanceof NumberItem numberItem ? numberItem.getUnit() : null;
|
||||
|
||||
Iterator<HistoricItem> it = result.iterator();
|
||||
@ -1406,10 +1400,7 @@ public class PersistenceExtensions {
|
||||
// avoid ArithmeticException if variance is less than zero
|
||||
if (dt != null && DecimalType.ZERO.compareTo(dt) <= 0) {
|
||||
BigDecimal deviation = dt.toBigDecimal().sqrt(MathContext.DECIMAL64);
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
if (baseItem instanceof NumberItem numberItem) {
|
||||
Unit<?> unit = numberItem.getUnit();
|
||||
if (unit != null) {
|
||||
@ -1520,29 +1511,27 @@ public class PersistenceExtensions {
|
||||
if (effectiveServiceId == null) {
|
||||
return null;
|
||||
}
|
||||
Iterable<HistoricItem> result = getAllStatesBetweenWithBoundaries(item, begin, end, effectiveServiceId);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
Iterator<HistoricItem> it = result.iterator();
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
ZonedDateTime beginTime = begin == null ? now : begin;
|
||||
ZonedDateTime endTime = end == null ? now : end;
|
||||
ZonedDateTime beginTime = Objects.requireNonNullElse(begin, now);
|
||||
ZonedDateTime endTime = Objects.requireNonNullElse(end, now);
|
||||
|
||||
if (beginTime.isEqual(endTime)) {
|
||||
HistoricItem historicItem = internalPersistedState(item, beginTime, effectiveServiceId);
|
||||
return historicItem != null ? historicItem.getState() : null;
|
||||
}
|
||||
|
||||
Iterable<HistoricItem> result = getAllStatesBetweenWithBoundaries(item, begin, end, effectiveServiceId);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
Iterator<HistoricItem> it = result.iterator();
|
||||
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
|
||||
HistoricItem lastItem = null;
|
||||
ZonedDateTime firstTimestamp = null;
|
||||
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = baseItem instanceof NumberItem numberItem ? numberItem.getUnit() : null;
|
||||
|
||||
while (it.hasNext()) {
|
||||
@ -1578,6 +1567,140 @@ public class PersistenceExtensions {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the median value of the state of a given {@link Item} since a certain point in time.
|
||||
* The default {@link PersistenceService} is used.
|
||||
*
|
||||
* @param item the {@link Item} to get the median value for
|
||||
* @param timestamp the point in time from which to search for the median value
|
||||
* @return the median value since <code>timestamp</code> or <code>null</code> if no
|
||||
* previous states could be found or if the default persistence service does not refer to an available
|
||||
* {@link QueryablePersistenceService}. The current state is included in the calculation.
|
||||
*/
|
||||
public static @Nullable State medianSince(Item item, ZonedDateTime timestamp) {
|
||||
return internalMedianBetween(item, timestamp, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the median value of the state of a given {@link Item} until a certain point in time.
|
||||
* The default {@link PersistenceService} is used.
|
||||
*
|
||||
* @param item the {@link Item} to get the median value for
|
||||
* @param timestamp the point in time to which to search for the median value
|
||||
* @return the median value until <code>timestamp</code> or <code>null</code> if no
|
||||
* future states could be found or if the default persistence service does not refer to an available
|
||||
* {@link QueryablePersistenceService}. The current state is included in the calculation.
|
||||
*/
|
||||
public static @Nullable State medianUntil(Item item, ZonedDateTime timestamp) {
|
||||
return internalMedianBetween(item, null, timestamp, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the median value of the state of a given {@link Item} between two certain points in time.
|
||||
* The default {@link PersistenceService} is used.
|
||||
*
|
||||
* @param item the {@link Item} to get the median value for
|
||||
* @param begin the point in time from which to start the summation
|
||||
* @param end the point in time to which to start the summation
|
||||
* @return the median value between <code>begin</code> and <code>end</code> or <code>null</code> if no
|
||||
* states could be found or if the default persistence service does not refer to an available
|
||||
* {@link QueryablePersistenceService}.
|
||||
*/
|
||||
public static @Nullable State medianBetween(Item item, ZonedDateTime begin, ZonedDateTime end) {
|
||||
return internalMedianBetween(item, begin, end, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the median value of the state of a given {@link Item} since a certain point in time.
|
||||
* The {@link PersistenceService} identified by the <code>serviceId</code> is used.
|
||||
*
|
||||
* @param item the {@link Item} to get the median value for
|
||||
* @param timestamp the point in time from which to search for the median value
|
||||
* @param serviceId the name of the {@link PersistenceService} to use
|
||||
* @return the median value since <code>timestamp</code>, or <code>null</code> if no
|
||||
* previous states could be found or if the persistence service given by <code>serviceId</code> does not
|
||||
* refer to an available {@link QueryablePersistenceService}. The current state is included in the
|
||||
* calculation.
|
||||
*/
|
||||
public static @Nullable State medianSince(Item item, ZonedDateTime timestamp, @Nullable String serviceId) {
|
||||
return internalMedianBetween(item, timestamp, null, serviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the median value of the state of a given {@link Item} until a certain point in time.
|
||||
* The {@link PersistenceService} identified by the <code>serviceId</code> is used.
|
||||
*
|
||||
* @param item the {@link Item} to get the median value for
|
||||
* @param timestamp the point in time to which to search for the median value
|
||||
* @param serviceId the name of the {@link PersistenceService} to use
|
||||
* @return the median value until <code>timestamp</code>, or <code>null</code> if no
|
||||
* future states could be found or if the persistence service given by <code>serviceId</code> does not
|
||||
* refer to an available {@link QueryablePersistenceService}. The current state is included in the
|
||||
* calculation.
|
||||
*/
|
||||
public static @Nullable State medianUntil(Item item, ZonedDateTime timestamp, @Nullable String serviceId) {
|
||||
return internalMedianBetween(item, null, timestamp, serviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the median value of the state of a given {@link Item} between two certain points in time.
|
||||
* The {@link PersistenceService} identified by the <code>serviceId</code> is used.
|
||||
*
|
||||
* @param item the {@link Item} to get the median value for
|
||||
* @param begin the point in time from which to start the summation
|
||||
* @param end the point in time to which to start the summation
|
||||
* @param serviceId the name of the {@link PersistenceService} to use
|
||||
* @return the median value between <code>begin</code> and <code>end</code>, or <code>null</code> if no
|
||||
* states could be found or if the persistence service given by <code>serviceId</code> does not
|
||||
* refer to an available {@link QueryablePersistenceService}
|
||||
*/
|
||||
public static @Nullable State medianBetween(Item item, ZonedDateTime begin, ZonedDateTime end,
|
||||
@Nullable String serviceId) {
|
||||
return internalMedianBetween(item, begin, end, serviceId);
|
||||
}
|
||||
|
||||
private static @Nullable State internalMedianBetween(Item item, @Nullable ZonedDateTime begin,
|
||||
@Nullable ZonedDateTime end, @Nullable String serviceId) {
|
||||
String effectiveServiceId = serviceId == null ? getDefaultServiceId() : serviceId;
|
||||
if (effectiveServiceId == null) {
|
||||
return null;
|
||||
}
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
ZonedDateTime beginTime = Objects.requireNonNullElse(begin, now);
|
||||
ZonedDateTime endTime = Objects.requireNonNullElse(end, now);
|
||||
|
||||
if (beginTime.isEqual(endTime)) {
|
||||
HistoricItem historicItem = internalPersistedState(item, beginTime, effectiveServiceId);
|
||||
return historicItem != null ? historicItem.getState() : null;
|
||||
}
|
||||
|
||||
Iterable<HistoricItem> result = getAllStatesBetween(item, beginTime, endTime, effectiveServiceId);
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = baseItem instanceof NumberItem numberItem ? numberItem.getUnit() : null;
|
||||
|
||||
List<BigDecimal> resultList = new ArrayList<>();
|
||||
result.forEach(hi -> {
|
||||
DecimalType dtState = getPersistedValue(hi, unit);
|
||||
if (dtState != null) {
|
||||
resultList.add(dtState.toBigDecimal());
|
||||
}
|
||||
});
|
||||
|
||||
BigDecimal median = Statistics.median(resultList);
|
||||
if (median != null) {
|
||||
if (unit != null) {
|
||||
return new QuantityType<>(median, unit);
|
||||
} else {
|
||||
return new DecimalType(median);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sum of the state of a given <code>item</code> since a certain point in time.
|
||||
* The default persistence service is used.
|
||||
@ -1675,10 +1798,7 @@ public class PersistenceExtensions {
|
||||
if (result != null) {
|
||||
Iterator<HistoricItem> it = result.iterator();
|
||||
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = baseItem instanceof NumberItem numberItem ? numberItem.getUnit() : null;
|
||||
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
@ -1800,10 +1920,7 @@ public class PersistenceExtensions {
|
||||
HistoricItem itemStart = internalPersistedState(item, begin, effectiveServiceId);
|
||||
HistoricItem itemStop = internalPersistedState(item, end, effectiveServiceId);
|
||||
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = baseItem instanceof NumberItem numberItem ? numberItem.getUnit() : null;
|
||||
|
||||
DecimalType valueStart = null;
|
||||
@ -2037,10 +2154,7 @@ public class PersistenceExtensions {
|
||||
return null;
|
||||
}
|
||||
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
Unit<?> unit = baseItem instanceof NumberItem numberItem ? numberItem.getUnit() : null;
|
||||
|
||||
HistoricItem itemStart = internalPersistedState(item, begin, effectiveServiceId);
|
||||
@ -2556,8 +2670,8 @@ public class PersistenceExtensions {
|
||||
return null;
|
||||
}
|
||||
|
||||
ZonedDateTime beginTime = (begin == null) ? now : begin;
|
||||
ZonedDateTime endTime = (end == null) ? now : end;
|
||||
ZonedDateTime beginTime = Objects.requireNonNullElse(begin, now);
|
||||
ZonedDateTime endTime = Objects.requireNonNullElse(end, now);
|
||||
|
||||
List<HistoricItem> betweenItemsList = new ArrayList<>();
|
||||
if (betweenItems != null) {
|
||||
@ -2616,10 +2730,7 @@ public class PersistenceExtensions {
|
||||
}
|
||||
|
||||
private static @Nullable DecimalType getItemValue(Item item) {
|
||||
Item baseItem = item;
|
||||
if (baseItem instanceof GroupItem groupItem) {
|
||||
baseItem = groupItem.getBaseItem();
|
||||
}
|
||||
Item baseItem = item instanceof GroupItem groupItem ? groupItem.getBaseItem() : item;
|
||||
if (baseItem instanceof NumberItem numberItem) {
|
||||
Unit<?> unit = numberItem.getUnit();
|
||||
if (unit != null) {
|
||||
|
@ -61,6 +61,9 @@ import org.openhab.core.types.State;
|
||||
* @author Jan N. Klug - Interval method tests and refactoring
|
||||
* @author Mark Herwege - Changed return types to State for some interval methods to also return unit
|
||||
* @author Mark Herwege - Extended for future dates
|
||||
* @author Mark Herwege - lastChange and nextChange methods
|
||||
* @author Mark Herwege - handle persisted GroupItem with QuantityType
|
||||
* @author Mark Herwege - add median methods
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@ -1875,6 +1878,260 @@ public class PersistenceExtensionsTest {
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianSinceDecimalType() {
|
||||
ZonedDateTime start = ZonedDateTime.of(BEFORE_START, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
double expected = median(BEFORE_START, null);
|
||||
State median = PersistenceExtensions.medianSince(numberItem, start, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
DecimalType dt = median.as(DecimalType.class);
|
||||
assertNotNull(dt);
|
||||
assertEquals(expected, dt.doubleValue(), 0.01);
|
||||
|
||||
start = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(HISTORIC_INTERMEDIATE_VALUE_1, null);
|
||||
median = PersistenceExtensions.medianSince(numberItem, start, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
dt = median.as(DecimalType.class);
|
||||
assertNotNull(dt);
|
||||
assertEquals(expected, dt.doubleValue(), 0.01);
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianSince(numberItem, start);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianUntilDecimalType() {
|
||||
ZonedDateTime end = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
double expected = median(null, FUTURE_INTERMEDIATE_VALUE_3);
|
||||
State median = PersistenceExtensions.medianUntil(numberItem, end, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
DecimalType dt = median.as(DecimalType.class);
|
||||
assertNotNull(dt);
|
||||
assertEquals(expected, dt.doubleValue(), 0.01);
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianUntil(numberItem, end);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianBetweenDecimalType() {
|
||||
ZonedDateTime beginStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0,
|
||||
ZoneId.systemDefault());
|
||||
ZonedDateTime endStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_2, 1, 1, 0, 0, 0, 0,
|
||||
ZoneId.systemDefault());
|
||||
|
||||
double expected = median(HISTORIC_INTERMEDIATE_VALUE_1, HISTORIC_INTERMEDIATE_VALUE_2);
|
||||
State median = PersistenceExtensions.medianBetween(numberItem, beginStored, endStored, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
DecimalType dt = median.as(DecimalType.class);
|
||||
assertNotNull(dt);
|
||||
assertEquals(expected, dt.doubleValue(), 0.01);
|
||||
|
||||
beginStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
endStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_4, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(FUTURE_INTERMEDIATE_VALUE_3, FUTURE_INTERMEDIATE_VALUE_4);
|
||||
|
||||
median = PersistenceExtensions.medianBetween(numberItem, beginStored, endStored, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
dt = median.as(DecimalType.class);
|
||||
assertNotNull(dt);
|
||||
assertThat(dt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
|
||||
beginStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
endStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(HISTORIC_INTERMEDIATE_VALUE_1, FUTURE_INTERMEDIATE_VALUE_3);
|
||||
|
||||
median = PersistenceExtensions.medianBetween(numberItem, beginStored, endStored, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
dt = median.as(DecimalType.class);
|
||||
assertNotNull(dt);
|
||||
assertThat(dt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianBetween(quantityItem, beginStored, endStored);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianSinceQuantityType() {
|
||||
ZonedDateTime start = ZonedDateTime.of(BEFORE_START, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
double expected = median(BEFORE_START, null);
|
||||
State median = PersistenceExtensions.medianSince(quantityItem, start, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
QuantityType<?> qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertEquals(expected, qt.doubleValue(), 0.01);
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
start = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(HISTORIC_INTERMEDIATE_VALUE_1, null);
|
||||
median = PersistenceExtensions.medianSince(quantityItem, start, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertEquals(expected, qt.doubleValue(), 0.01);
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianSince(quantityItem, start);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianUntilQuantityType() {
|
||||
ZonedDateTime end = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
double expected = median(null, FUTURE_INTERMEDIATE_VALUE_3);
|
||||
State median = PersistenceExtensions.medianUntil(quantityItem, end, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
QuantityType<?> qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertEquals(expected, qt.doubleValue(), 0.01);
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianUntil(quantityItem, end);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianBetweenQuantityType() {
|
||||
ZonedDateTime beginStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0,
|
||||
ZoneId.systemDefault());
|
||||
ZonedDateTime endStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_2, 1, 1, 0, 0, 0, 0,
|
||||
ZoneId.systemDefault());
|
||||
double expected = median(HISTORIC_INTERMEDIATE_VALUE_1, HISTORIC_INTERMEDIATE_VALUE_2);
|
||||
State median = PersistenceExtensions.medianBetween(quantityItem, beginStored, endStored, SERVICE_ID);
|
||||
|
||||
assertNotNull(median);
|
||||
QuantityType<?> qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertThat(qt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
beginStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
endStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_4, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(FUTURE_INTERMEDIATE_VALUE_3, FUTURE_INTERMEDIATE_VALUE_4);
|
||||
|
||||
median = PersistenceExtensions.medianBetween(quantityItem, beginStored, endStored, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertThat(qt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
beginStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
endStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(HISTORIC_INTERMEDIATE_VALUE_1, FUTURE_INTERMEDIATE_VALUE_3);
|
||||
|
||||
median = PersistenceExtensions.medianBetween(quantityItem, beginStored, endStored, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertThat(qt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianBetween(quantityItem, beginStored, endStored);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianSinceGroupQuantityType() {
|
||||
ZonedDateTime start = ZonedDateTime.of(BEFORE_START, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
double expected = median(BEFORE_START, null);
|
||||
State median = PersistenceExtensions.medianSince(groupQuantityItem, start, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
QuantityType<?> qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertEquals(expected, qt.doubleValue(), 0.01);
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
start = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(HISTORIC_INTERMEDIATE_VALUE_1, null);
|
||||
median = PersistenceExtensions.medianSince(groupQuantityItem, start, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertEquals(expected, qt.doubleValue(), 0.01);
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianSince(groupQuantityItem, start);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianUntilGroupQuantityType() {
|
||||
ZonedDateTime end = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
double expected = median(null, FUTURE_INTERMEDIATE_VALUE_3);
|
||||
State median = PersistenceExtensions.medianUntil(groupQuantityItem, end, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
QuantityType<?> qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertEquals(expected, qt.doubleValue(), 0.01);
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianUntil(groupQuantityItem, end);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianBetweenGroupQuantityType() {
|
||||
ZonedDateTime beginStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0,
|
||||
ZoneId.systemDefault());
|
||||
ZonedDateTime endStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_2, 1, 1, 0, 0, 0, 0,
|
||||
ZoneId.systemDefault());
|
||||
double expected = median(HISTORIC_INTERMEDIATE_VALUE_1, HISTORIC_INTERMEDIATE_VALUE_2);
|
||||
State median = PersistenceExtensions.medianBetween(groupQuantityItem, beginStored, endStored, SERVICE_ID);
|
||||
|
||||
assertNotNull(median);
|
||||
QuantityType<?> qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertThat(qt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
beginStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
endStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_4, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(FUTURE_INTERMEDIATE_VALUE_3, FUTURE_INTERMEDIATE_VALUE_4);
|
||||
|
||||
median = PersistenceExtensions.medianBetween(groupQuantityItem, beginStored, endStored, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertThat(qt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
beginStored = ZonedDateTime.of(HISTORIC_INTERMEDIATE_VALUE_1, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
endStored = ZonedDateTime.of(FUTURE_INTERMEDIATE_VALUE_3, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
|
||||
expected = median(HISTORIC_INTERMEDIATE_VALUE_1, FUTURE_INTERMEDIATE_VALUE_3);
|
||||
|
||||
median = PersistenceExtensions.medianBetween(groupQuantityItem, beginStored, endStored, SERVICE_ID);
|
||||
assertNotNull(median);
|
||||
qt = median.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertThat(qt.doubleValue(), is(closeTo(expected, 0.01)));
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
|
||||
// default persistence service
|
||||
median = PersistenceExtensions.medianBetween(groupQuantityItem, beginStored, endStored);
|
||||
assertNull(median);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMedianBetweenZeroDuration() {
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
State state = PersistenceExtensions.medianBetween(quantityItem, now, now, SERVICE_ID);
|
||||
assertNotNull(state);
|
||||
QuantityType<?> qt = state.as(QuantityType.class);
|
||||
assertNotNull(qt);
|
||||
assertEquals(HISTORIC_END, qt.doubleValue(), 0.01);
|
||||
assertEquals(SIUnits.CELSIUS, qt.getUnit());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSumSinceDecimalType() {
|
||||
State sum = PersistenceExtensions.sumSince(numberItem,
|
||||
|
@ -269,4 +269,19 @@ public class TestPersistenceService implements QueryablePersistenceService {
|
||||
long duration = Duration.between(beginDate, endDate).toMillis();
|
||||
return 1.0 * sum / duration;
|
||||
}
|
||||
|
||||
static double median(@Nullable Integer beginYear, @Nullable Integer endYear) {
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
int begin = beginYear != null ? beginYear : now.getYear() + 1;
|
||||
int end = endYear != null ? endYear : now.getYear();
|
||||
long[] values = LongStream.range(begin, end + 1)
|
||||
.filter(v -> ((v >= HISTORIC_START && v <= HISTORIC_END) || (v >= FUTURE_START && v <= FUTURE_END)))
|
||||
.sorted().toArray();
|
||||
int length = values.length;
|
||||
if (length % 2 == 1) {
|
||||
return values[values.length / 2];
|
||||
} else {
|
||||
return 0.5 * (values[values.length / 2] + values[values.length / 2 - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.util;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link Statistics} is a class with statistics helper methods.
|
||||
*
|
||||
* @author Mark Herwege - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Statistics {
|
||||
|
||||
/**
|
||||
* Find the median in a list of values
|
||||
*
|
||||
* @param inputList
|
||||
* @return median of the values, null if the list is empty
|
||||
*/
|
||||
public static @Nullable BigDecimal median(List<BigDecimal> inputList) {
|
||||
ArrayList<BigDecimal> bdList = new ArrayList<>(inputList); // Make a copy that will get reordered
|
||||
int size = bdList.size();
|
||||
if (size >= 0) {
|
||||
int k = size / 2;
|
||||
BigDecimal median = null;
|
||||
if (size % 2 == 1) {
|
||||
median = Statistics.quickSelect(bdList, 0, size - 1, k, false);
|
||||
} else {
|
||||
median = Statistics.quickSelect(bdList, 0, size - 1, k, true);
|
||||
if (median != null) {
|
||||
// quickSelect has forced the k-1 element to be in the right place
|
||||
median = median.add(bdList.get(k - 1)).divide(BigDecimal.valueOf(2));
|
||||
}
|
||||
}
|
||||
return median;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the k-smallest element between indexes l and r in a list. This is an implementation of the quickSelect
|
||||
* algorithm. If the forcePreviousOrder parameter is set to true, put the element before the k-smallest element at
|
||||
* position k-1 in bdList. This is useful to calculate the median or percentile on a list with an uneven number of
|
||||
* elements. The median will then be the sum of the returned value and the element at position k-1 divided by 2.
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Quickselect and https://gist.github.com/unnikked/14c19ba13f6a4bfd00a3
|
||||
*
|
||||
* @param bdList, list elements will be reordered in place
|
||||
* @param l index of left most element in list to consider
|
||||
* @param r index of right most element in list to consider
|
||||
* @param k
|
||||
* @param forcePreviousOrder positions the k-1 element in the right place if true, useful to calculate median on
|
||||
* list with even length
|
||||
* @return
|
||||
*/
|
||||
static @Nullable BigDecimal quickSelect(ArrayList<BigDecimal> bdList, int l, int r, int k,
|
||||
boolean forcePreviousOrder) {
|
||||
if (r < 0) {
|
||||
return null;
|
||||
} else if (r == 0) {
|
||||
return bdList.get(r);
|
||||
}
|
||||
|
||||
int left = l;
|
||||
int right = r;
|
||||
for (;;) {
|
||||
int pivotIndex = left; // Textbook quickselect algorithm uses a random pivot index in the left to right
|
||||
// range. The random generation adds time and does not change the algorithm result,
|
||||
// so just pick left.
|
||||
pivotIndex = partition(bdList, left, right, pivotIndex, forcePreviousOrder);
|
||||
|
||||
if (k == pivotIndex) {
|
||||
return bdList.get(k);
|
||||
} else if (k < pivotIndex) {
|
||||
right = pivotIndex - 1;
|
||||
} else {
|
||||
left = pivotIndex + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int partition(ArrayList<BigDecimal> bdList, int left, int right, int pivotIndex,
|
||||
boolean forcePreviousOrder) {
|
||||
BigDecimal pivotValue = bdList.get(pivotIndex);
|
||||
swap(bdList, pivotIndex, right); // Move pivot to end
|
||||
int beforePivotIndex = left;
|
||||
int storeIndex = left;
|
||||
for (int i = left; i < right; i++) {
|
||||
if (bdList.get(i).compareTo(pivotValue) < 0) {
|
||||
if (forcePreviousOrder && (bdList.get(i).compareTo(bdList.get(beforePivotIndex)) > 0)) {
|
||||
beforePivotIndex = storeIndex;
|
||||
}
|
||||
swap(bdList, storeIndex, i);
|
||||
storeIndex++;
|
||||
}
|
||||
}
|
||||
swap(bdList, right, storeIndex); // Move pivot to its final place
|
||||
if (forcePreviousOrder && (storeIndex > beforePivotIndex)) {
|
||||
swap(bdList, beforePivotIndex, storeIndex - 1);
|
||||
}
|
||||
return storeIndex;
|
||||
}
|
||||
|
||||
private static void swap(ArrayList<BigDecimal> bdList, int i, int j) {
|
||||
if (i != j) {
|
||||
BigDecimal tmp = bdList.get(i);
|
||||
bdList.set(i, bdList.get(j));
|
||||
bdList.set(j, tmp);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.core.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* The {@link StatisticsTest} is a test class for the statistics helper methods
|
||||
*
|
||||
* @author Mark Herwege - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class StatisticsTest {
|
||||
|
||||
@Test
|
||||
public void testQuickSelect() {
|
||||
List<BigDecimal> randomList = new Random().doubles(100, 0, 100).mapToObj(v -> BigDecimal.valueOf(v)).toList();
|
||||
int iterations = 50;
|
||||
|
||||
int size = randomList.size();
|
||||
int k = size / 2; // median
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
List<BigDecimal> baseList = randomList.stream().sorted().toList();
|
||||
long endTime = System.nanoTime();
|
||||
long duration = endTime - startTime;
|
||||
int expected = baseList.get(k).intValue();
|
||||
int prevExpected = baseList.get(k - 1).intValue();
|
||||
|
||||
long durationNoForcePrevious = 0;
|
||||
long durationForcePrevious = 0;
|
||||
// Iterate a few times with reshuffled list to exclude impact of initial ordering
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
ArrayList<BigDecimal> bdList = new ArrayList<>(baseList);
|
||||
Collections.shuffle(bdList);
|
||||
startTime = System.nanoTime();
|
||||
BigDecimal bd = Statistics.quickSelect(bdList, 0, size - 1, k, false);
|
||||
endTime = System.nanoTime();
|
||||
durationNoForcePrevious += endTime - startTime;
|
||||
assertNotNull(bd);
|
||||
int result = bd.intValue();
|
||||
assertEquals(expected, result);
|
||||
|
||||
bdList = new ArrayList<>(baseList); // recreate as order may have changed
|
||||
Collections.shuffle(bdList);
|
||||
startTime = System.nanoTime();
|
||||
bd = Statistics.quickSelect(bdList, 0, size - 1, k, true);
|
||||
endTime = System.nanoTime();
|
||||
durationForcePrevious += endTime - startTime;
|
||||
assertNotNull(bd);
|
||||
result = bd.intValue();
|
||||
assertEquals(expected, result);
|
||||
assertEquals(prevExpected, bdList.get(k - 1).intValue());
|
||||
}
|
||||
|
||||
PrintStream out = System.out;
|
||||
if (out != null) {
|
||||
out.print("List size: ");
|
||||
out.print(size);
|
||||
out.print(", iterations: ");
|
||||
out.println(iterations);
|
||||
out.print(" Stream sort duration (ns): ");
|
||||
out.println(duration);
|
||||
out.print(" Quickselect average duration (ns): ");
|
||||
out.println(durationNoForcePrevious / iterations);
|
||||
out.print(" Quickselect force previous order average duration (ns): ");
|
||||
out.println(durationForcePrevious / iterations);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user