diff --git a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java index 4e3344230a5..cb7901c2516 100644 --- a/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java +++ b/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/extensions/PersistenceExtensions.java @@ -1520,11 +1520,6 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable if (effectiveServiceId == null) { return null; } - Iterable result = getAllStatesBetweenWithBoundaries(item, begin, end, effectiveServiceId); - if (result == null) { - return null; - } - Iterator it = result.iterator(); ZonedDateTime now = ZonedDateTime.now(); ZonedDateTime beginTime = begin == null ? now : begin; ZonedDateTime endTime = end == null ? now : end; @@ -1534,6 +1529,12 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable return historicItem != null ? historicItem.getState() : null; } + Iterable result = getAllStatesBetweenWithBoundaries(item, begin, end, effectiveServiceId); + if (result == null) { + return null; + } + Iterator it = result.iterator(); + BigDecimal sum = BigDecimal.ZERO; HistoricItem lastItem = null; @@ -1578,6 +1579,214 @@ private static void internalPersist(Item item, TimeSeries timeSeries, @Nullable 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 timestamp or null 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 timestamp or null 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 begin and end or null 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 serviceId 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 timestamp, or null if no + * previous states could be found or if the persistence service given by serviceId 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 serviceId 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 timestamp, or null if no + * future states could be found or if the persistence service given by serviceId 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 serviceId 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 begin and end, or null if no + * states could be found or if the persistence service given by serviceId 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 = begin == null ? now : begin; + ZonedDateTime endTime = end == null ? now : end; + + if (beginTime.isEqual(endTime)) { + HistoricItem historicItem = internalPersistedState(item, beginTime, effectiveServiceId); + return historicItem != null ? historicItem.getState() : null; + } + + Iterable result = getAllStatesBetween(item, beginTime, endTime, effectiveServiceId); + if (result == null) { + return null; + } + + Item baseItem = item; + if (baseItem instanceof GroupItem groupItem) { + baseItem = groupItem.getBaseItem(); + } + Unit unit = baseItem instanceof NumberItem numberItem ? numberItem.getUnit() : null; + + ArrayList resultList = new ArrayList<>(); + result.forEach(hi -> { + DecimalType dtState = getPersistedValue(hi, unit); + if (dtState != null) { + resultList.add(dtState.toBigDecimal()); + } + }); + + int size = resultList.size(); + if (size >= 0) { + int k = size / 2; + BigDecimal median = QuickSelect.quickSelect(resultList, k); + if (size % 2 == 0) { + BigDecimal median2 = QuickSelect.quickSelect(resultList, k - 1); + if ((median != null) && (median2 != null)) { + median = median.add(median2).divide(new BigDecimal(2)); + } + } + + if (median != null) { + if (unit != null) { + return new QuantityType<>(median, unit); + } else { + return new DecimalType(median); + } + } + } + return null; + } + + /** + * Class implementing the quickSelect algorithm. + * See https://en.wikipedia.org/wiki/Quickselect and https://gist.github.com/unnikked/14c19ba13f6a4bfd00a3 + */ + private static class QuickSelect { + /** + * Find the k-smallest element in a list. + * + * @param bdList, list elements will be reordered in place + * @param k + * @return + */ + static @Nullable BigDecimal quickSelect(ArrayList bdList, int k) { + int left = 0; + int right = bdList.size() - 1; + if (right < 0) { + return null; + } else if (right == 0) { + return bdList.get(right); + } + + for (;;) { + int pivotIndex = randomPivot(left, right); + pivotIndex = partition(bdList, left, right, pivotIndex); + + if (k == pivotIndex) { + return bdList.get(k); + } else if (k < pivotIndex) { + right = pivotIndex - 1; + } else { + left = pivotIndex + 1; + } + } + } + + private static int partition(ArrayList bdList, int left, int right, int pivotIndex) { + BigDecimal pivotValue = bdList.get(pivotIndex); + swap(bdList, pivotIndex, right); // move pivot to end + int storeIndex = left; + for (int i = left; i < right; i++) { + if (bdList.get(i).compareTo(pivotValue) < 0) { + swap(bdList, storeIndex, i); + storeIndex++; + } + } + swap(bdList, right, storeIndex); // Move pivot to its final place + return storeIndex; + } + + private static void swap(ArrayList bdList, int a, int b) { + BigDecimal tmp = bdList.get(a); + bdList.set(a, bdList.get(b)); + bdList.set(b, tmp); + } + + private static int randomPivot(int left, int right) { + return left + (int) Math.floor(Math.random() * (right - left + 1)); + } + } + /** * Gets the sum of the state of a given item since a certain point in time. * The default persistence service is used. diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java index fd28de805d9..9145727932c 100644 --- a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/PersistenceExtensionsTest.java @@ -1875,6 +1875,260 @@ public void testAverageBetweenZeroDuration() { 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, diff --git a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java index 74ee49a33bc..92ca504b610 100644 --- a/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java +++ b/bundles/org.openhab.core.persistence/src/test/java/org/openhab/core/persistence/extensions/TestPersistenceService.java @@ -269,4 +269,20 @@ static double average(@Nullable Integer beginYear, @Nullable Integer endYear) { 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]); + } + + } }