/**
* Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.collect.timeseries;
import static java.util.stream.Collectors.partitioningBy;
import java.time.LocalDate;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.OptionalDouble;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoublePredicate;
import java.util.function.DoubleUnaryOperator;
import java.util.function.Function;
import java.util.function.ObjDoubleConsumer;
import java.util.stream.Collector;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.function.ObjDoublePredicate;
import com.opengamma.strata.collect.tuple.Pair;
/**
* Interface for all local date time-series types containing
* {@code double} values.
* <p>
* A time-series is similar to both a {@code SortedMap} of value keyed
* by date-time and a {@code List} of date-time to value pairs. As such,
* the date/times do not have to be evenly spread over time within the series.
* <p>
* The distribution of the data will influence which implementation
* is most appropriate.
* <p>
* Implementations must be immutable and thread-safe beans.
* <p>
* Note that {@link Double#NaN} is used internally as a sentinel
* value and is therefore not allowed as a value.
*/
public interface LocalDateDoubleTimeSeries {
/**
* Returns an empty time-series. Generally a singleton instance
* is returned.
*
* @return an empty time-series
*/
public static LocalDateDoubleTimeSeries empty() {
return SparseLocalDateDoubleTimeSeries.EMPTY;
}
/**
* Obtains a time-series containing a single date and value.
*
* @param date the singleton date
* @param value the singleton value
* @return the time-series
*/
public static LocalDateDoubleTimeSeries of(LocalDate date, double value) {
ArgChecker.notNull(date, "date");
return builder().put(date, value).build();
}
/**
* Creates an empty builder, used to create time-series.
* <p>
* The builder has methods to create and modify a time-series.
*
* @return the time-series builder
*/
public static LocalDateDoubleTimeSeriesBuilder builder() {
return new LocalDateDoubleTimeSeriesBuilder();
}
/**
* Returns a collector that can be used to create a time-series from a stream of points.
*
* @return the time-series collector
*/
public static Collector<LocalDateDoublePoint, LocalDateDoubleTimeSeriesBuilder, LocalDateDoubleTimeSeries> collector() {
return Collector.of(
LocalDateDoubleTimeSeriesBuilder::new,
LocalDateDoubleTimeSeriesBuilder::put,
LocalDateDoubleTimeSeriesBuilder::putAll,
LocalDateDoubleTimeSeriesBuilder::build);
}
//-------------------------------------------------------------------------
/**
* Return the size of this time-series.
*
* @return the size of the time-series
*/
public abstract int size();
/**
* Indicates if this time-series is empty.
*
* @return true if the time-series contains no entries
*/
public abstract boolean isEmpty();
/**
* Checks if this time-series contains a value for the specified date.
*
* @param date the date to check for
* @return true if there is a value associated with the date
*/
public abstract boolean containsDate(LocalDate date);
/**
* Gets the value associated with the specified date.
* <p>
* The result is an {@link OptionalDouble} which avoids the need to handle null
* or exceptions. Use {@code isPresent()} to check whether the value is present.
* Use {@code orElse(double)} to default a missing value.
*
* @param date the date to get the value for
* @return the value associated with the date, optional empty if the date is not present
*/
public abstract OptionalDouble get(LocalDate date);
//-------------------------------------------------------------------------
/**
* Get the earliest date contained in this time-series.
* <p>
* If the time-series is empty then {@link NoSuchElementException} will be thrown.
*
* @return the earliest date contained in this time-series
* @throws NoSuchElementException if the time-series is empty
*/
public default LocalDate getEarliestDate() {
return dates().findFirst()
.orElseThrow(() -> new NoSuchElementException("Unable to return earliest date, time-series is empty"));
}
/**
* Get the value held for the earliest date contained in this time-series.
* <p>
* If the time-series is empty then {@link NoSuchElementException} will be thrown.
*
* @return the value held for the earliest date contained in this time-series
* @throws NoSuchElementException if the time-series is empty
*/
public default double getEarliestValue() {
return values().findFirst()
.orElseThrow(() -> new NoSuchElementException("Unable to return earliest value, time-series is empty"));
}
/**
* Get the latest date contained in this time-series.
* <p>
* If the time-series is empty then {@link NoSuchElementException} will be thrown.
*
* @return the latest date contained in this time-series
* @throws NoSuchElementException if the time-series is empty
*/
public abstract LocalDate getLatestDate();
/**
* Get the value held for the latest date contained in this time-series.
* <p>
* If the time-series is empty then {@link NoSuchElementException} will be thrown.
*
* @return the value held for the latest date contained in this time-series
* @throws NoSuchElementException if the time-series is empty
*/
public abstract double getLatestValue();
//-------------------------------------------------------------------------
/**
* Gets part of this series as a sub-series between two dates.
* <p>
* The date-times do not have to match exactly.
* The sub-series contains all entries between the two dates using a half-open interval.
* The start date is included, the end date is excluded.
* The dates may be before or after the end of the time-series.
* <p>
* To obtain the series before a specific date, used {@code LocalDate.MIN} as the first argument.
* To obtain the series from a specific date, used {@code LocalDate.MAX} as the second argument.
*
* @param startInclusive the start date, inclusive
* @param endExclusive the end date, exclusive
* @return the sub-series between the dates
* @throws IllegalArgumentException if the end is before the start
*/
public abstract LocalDateDoubleTimeSeries subSeries(LocalDate startInclusive, LocalDate endExclusive);
/**
* Gets part of this series as a sub-series, choosing the earliest entries.
* <p>
* The sub-series contains the earliest part of the series up to the specified number of points.
* If the series contains less points than the number requested, the whole time-series is returned.
*
* @param numPoints the number of items to select, zero or greater
* @return the sub-series of the requested size starting with the earliest entry
* @throws IllegalArgumentException if the number of items is less than zero
*/
public abstract LocalDateDoubleTimeSeries headSeries(int numPoints);
/**
* Gets part of this series as a sub-series, choosing the latest entries.
* <p>
* The sub-series contains the latest part of the series up to the specified number of points.
* If the series contains less points than the number requested, the whole time-series is returned.
*
* @param numPoints the number of items to select, zero or greater
* @return the sub-series of the requested size ending with the latest entry
* @throws IllegalArgumentException if the number of items is less than zero
*/
public abstract LocalDateDoubleTimeSeries tailSeries(int numPoints);
//-------------------------------------------------------------------------
/**
* Returns a stream over the points of this time-series.
* <p>
* This provides access to the entire time-series.
*
* @return a stream over the points of this time-series
*/
public abstract Stream<LocalDateDoublePoint> stream();
/**
* Returns a stream over the dates of this time-series.
* <p>
* This is most useful to summarize the dates in the stream, such as calculating
* the maximum or minimum date, or searching for a specific value.
*
* @return a stream over the dates of this time-series
*/
public abstract Stream<LocalDate> dates();
/**
* Returns a stream over the values of this time-series.
* <p>
* This is most useful to summarize the values in the stream, such as calculating
* the maximum, minimum or average value, or searching for a specific value.
*
* @return a stream over the values of this time-series
*/
public abstract DoubleStream values();
//-------------------------------------------------------------------------
/**
* Applies an action to each pair in the time series.
* <p>
* This is generally used to apply a mathematical operation to the values.
* For example, the operator could multiply each value by a constant, or take the inverse.
* <pre>
* base.forEach((date, value) -> System.out.println(date + "=" + value));
* </pre>
*
* @param action the action to be applied to each pair
*/
public abstract void forEach(ObjDoubleConsumer<LocalDate> action);
/**
* Applies an operation to each date in the time series which creates a new date, returning a new time series
* with the new dates and the points from this time series.
* <p>
* This operation creates a new time series with the same data but the dates moved.
* <p>
* The operation must not change the dates in a way that reorders them. The mapped dates must be in ascending
* order or an exception is thrown.
* <pre>
* updatedSeries = timeSeries.mapDates(date -> date.plusYears(1));
* </pre>
*
* @param mapper the operation applied to each date in the time series
* @return a copy of this time series with new dates
*/
public abstract LocalDateDoubleTimeSeries mapDates(Function<? super LocalDate, ? extends LocalDate> mapper);
/**
* Applies an operation to each value in the time series.
* <p>
* This is generally used to apply a mathematical operation to the values.
* For example, the operator could multiply each value by a constant, or take the inverse.
* <pre>
* multiplied = base.mapValues(value -> value * 3);
* </pre>
*
* @param mapper the operator to be applied to the values
* @return a copy of this series with the mapping applied to the original values
*/
public abstract LocalDateDoubleTimeSeries mapValues(DoubleUnaryOperator mapper);
/**
* Create a new time-series by filtering this one.
* <p>
* The time-series can be filtered by both date and value.
* Note that if filtering by date range is required, it is likely to be more efficient to
* use {@link #subSeries(LocalDate, LocalDate)} as that avoids traversal of the whole series.
*
* @param predicate the predicate to use to the filter the elements of the series
* @return a filtered version of the series
*/
public abstract LocalDateDoubleTimeSeries filter(ObjDoublePredicate<LocalDate> predicate);
//-------------------------------------------------------------------------
/**
* Obtains the intersection of a pair of time series.
* <p>
* This returns a time-series with the intersection of the dates of the two inputs.
* The operator is invoked to combine the values.
*
* @param other the time-series to combine with
* @param mapper the function to be used to combine the values
* @return a new time-series containing the dates in common between the
* input series with their values combined together using the function
*/
public default LocalDateDoubleTimeSeries intersection(LocalDateDoubleTimeSeries other, DoubleBinaryOperator mapper) {
ArgChecker.notNull(other, "other");
ArgChecker.notNull(mapper, "mapper");
return new LocalDateDoubleTimeSeriesBuilder()
.putAll(stream()
.filter(pt -> other.containsDate(pt.getDate()))
.map(pt -> LocalDateDoublePoint.of(
pt.getDate(), mapper.applyAsDouble(pt.getValue(), other.get(pt.getDate()).getAsDouble()))))
.build();
}
/**
* Obtains the union of a pair of time series.
* <p>
* This returns a time-series with the union of the dates of the two inputs.
* When the same date occurs in both time-series, the operator is invoked to combine the values.
*
* @param other the time-series to combine with
* @param mapper the function to be used to combine the values
* @return a new time-series containing the dates in common between the
* input series with their values combined together using the function
*/
public default LocalDateDoubleTimeSeries union(LocalDateDoubleTimeSeries other, DoubleBinaryOperator mapper) {
ArgChecker.notNull(other, "other");
ArgChecker.notNull(mapper, "mapper");
LocalDateDoubleTimeSeriesBuilder builder = new LocalDateDoubleTimeSeriesBuilder(stream());
other.stream().forEach(pt -> builder.merge(pt, mapper));
return builder.build();
}
/**
* Partition the time-series into a pair of distinct series using a predicate.
* <p>
* Points in the time-series which match the predicate will be put into the first series,
* whilst those points which do not match will be put into the second.
*
* @param predicate predicate used to test the points in the time-series
* @return a {@code Pair} containing two time-series. The first is a series
* made of all the points in this series which match the predicate. The
* second is a series made of the points which do not match.
*/
public default Pair<LocalDateDoubleTimeSeries, LocalDateDoubleTimeSeries> partition(
ObjDoublePredicate<LocalDate> predicate) {
Map<Boolean, LocalDateDoubleTimeSeries> partitioned = stream()
.collect(
partitioningBy(
pt -> predicate.test(pt.getDate(), pt.getValue()),
LocalDateDoubleTimeSeries.collector()));
return Pair.of(partitioned.get(true), partitioned.get(false));
}
/**
* Partition the time-series into a pair of distinct series using a predicate.
* <p>
* Points in the time-series whose values match the predicate will be put into the first series,
* whilst those points whose values do not match will be put into the second.
*
* @param predicate predicate used to test the points in the time-series
* @return a {@code Pair} containing two time-series. The first is a series
* made of all the points in this series which match the predicate. The
* second is a series made of the points which do not match.
*/
public default Pair<LocalDateDoubleTimeSeries, LocalDateDoubleTimeSeries> partitionByValue(
DoublePredicate predicate) {
return partition((obj, value) -> predicate.test(value));
}
/**
* Return a builder populated with the values from this series.
* <p>
* This can be used to mutate the time-series.
*
* @return a builder containing the point from this time-series
*/
public abstract LocalDateDoubleTimeSeriesBuilder toBuilder();
}