Median action in persistence extensions (#4345)

* median persistence extension

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege 2024-08-24 10:55:06 +02:00 committed by GitHub
parent b8e0f94cb0
commit b63fa473b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 646 additions and 46 deletions

View File

@ -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) {

View File

@ -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,

View File

@ -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]);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}