/**
* 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 com.opengamma.strata.collect.Guavate.toImmutableList;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.temporal.ChronoUnit.DAYS;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.OptionalDouble;
import java.util.Set;
import java.util.function.DoubleUnaryOperator;
import java.util.function.Function;
import java.util.function.ObjDoubleConsumer;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.joda.beans.Bean;
import org.joda.beans.BeanBuilder;
import org.joda.beans.BeanDefinition;
import org.joda.beans.ImmutableBean;
import org.joda.beans.ImmutableConstructor;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaProperty;
import org.joda.beans.Property;
import org.joda.beans.PropertyDefinition;
import org.joda.beans.impl.direct.DirectFieldsBeanBuilder;
import org.joda.beans.impl.direct.DirectMetaBean;
import org.joda.beans.impl.direct.DirectMetaProperty;
import org.joda.beans.impl.direct.DirectMetaPropertyMap;
import com.google.common.collect.Ordering;
import com.google.common.primitives.Doubles;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.Messages;
import com.opengamma.strata.collect.function.ObjDoublePredicate;
/**
* An immutable implementation of {@code DoubleTimeSeries} where the
* data stored is expected to be dense. For example, points for every
* working day in a month. If sparser data is being used then
* {@link SparseLocalDateDoubleTimeSeries} is likely to be a better
* choice for the data.
* <p>
* This implementation uses arrays internally.
*/
@BeanDefinition(builderScope = "private", metaScope = "package")
final class DenseLocalDateDoubleTimeSeries
implements ImmutableBean, LocalDateDoubleTimeSeries, Serializable {
/**
* Enum indicating whether there are positions in the points
* array for weekends and providing the different date
* calculations for each case.
*/
enum DenseTimeSeriesCalculation {
/**
* Data is not held for weekends.
*/
SKIP_WEEKENDS {
@Override
int calculatePosition(LocalDate startDate, LocalDate date) {
int unadjusted = (int) DAYS.between(startDate, date);
// If the day for the start date is after the day of the date of
// interest then there is an additional weekend that the
// integer division will not handle
// compare:
// Tues 8th -> Wed 16th, 8 days span, 8 / 7 = 1 weekend, correct so not further adjustment
// Tues 8th -> Mon 14th, 6 days span, 6 / 7 = 0 weekend, incorrect so we need to add adjustment
int weekendAdjustment = startDate.getDayOfWeek().compareTo(date.getDayOfWeek()) > 0 ? 1 : 0;
int numWeekends = (unadjusted / 7) + weekendAdjustment;
return unadjusted - (2 * numWeekends);
}
@Override
LocalDate calculateDateFromPosition(LocalDate startDate, int position) {
int numWeekends = position / 5;
int remaining = position % 5;
// As above we add adjustment for an uncaptured weekend
int endPointAdjustment = (remaining < (6 - startDate.get(DAY_OF_WEEK))) ? 0 : 2;
return startDate.plusDays((7 * numWeekends) + remaining + endPointAdjustment);
}
@Override
boolean allowsDate(LocalDate date) {
return !isWeekend(date);
}
@Override
public LocalDate adjustDate(LocalDate date) {
return allowsDate(date) ? date : date.plusDays(8 - date.get(DAY_OF_WEEK));
}
},
/**
* Data is held for weekends.
*/
INCLUDE_WEEKENDS {
@Override
int calculatePosition(LocalDate startDate, LocalDate date) {
return (int) DAYS.between(startDate, date);
}
@Override
LocalDate calculateDateFromPosition(LocalDate startDate, int position) {
return startDate.plusDays(position);
}
@Override
boolean allowsDate(LocalDate date) {
return true;
}
@Override
public LocalDate adjustDate(LocalDate date) {
return date;
}
};
/**
* Calculates the position in the array where the supplied date should
* be located given a start date. As no information is held about the
* actual array, callers must check array bounds.
*
* @param startDate the start date for the series (the value for this
* entry will be stored at position 0 in the array)
* @param date the date to calculate a position for
* @return the position in the array where the date would be located
*/
abstract int calculatePosition(LocalDate startDate, LocalDate date);
/**
* Given a start date and a position in an array, calculate what date
* the position holds data for.
*
* @param startDate the start date for the series (the value for this
* entry will be stored at position 0 in the array)
* @param position the position in the array to calculate a date for
* @return the date the position in the array holds data for
*/
abstract LocalDate calculateDateFromPosition(LocalDate startDate, int position);
/**
* Indicates if the specified date would be a possible date
* for the calculation.
*
* @param date the date to check
* @return true if the calculation would allow the date
*/
abstract boolean allowsDate(LocalDate date);
/**
* Adjusts the supplied data such that it is a valid
* date from the calculation's point of view.
*
* @param date the date to adjust
* @return the adjusted date
*/
public abstract LocalDate adjustDate(LocalDate date);
// Sufficient for the moment, in the future we may need to
// vary depending on a non-Western weekend
private static boolean isWeekend(LocalDate date) {
return date.get(DAY_OF_WEEK) > 5;
}
}
/**
* Date corresponding to first element in the array. All other
* values can be calculated using date arithmetic to find
* correct point.
*/
@PropertyDefinition(validate = "notNull")
private final LocalDate startDate;
/**
* The values in the series.
* The date for each value is calculated using the position
* in the array and the start date.
*/
@PropertyDefinition(get = "private", validate = "notNull")
private final double[] points;
/**
* Whether we should store data for the weekends (NaN will be stored
* if no data is available).
*/
@PropertyDefinition(get = "private", validate = "notNull")
private final DenseTimeSeriesCalculation dateCalculation;
/**
* Package protected factory method intended to be called
* by the {@link LocalDateDoubleTimeSeriesBuilder}. As such
* all the information passed is assumed to be consistent.
*
* @param startDate the earliest date included in the time-series
* @param endDate the latest date included in the time-series
* @param values stream holding the time-series points
* @param dateCalculation the date calculation method to be used
* @return a new time-series
*/
static LocalDateDoubleTimeSeries of(
LocalDate startDate,
LocalDate endDate,
Stream<LocalDateDoublePoint> values,
DenseTimeSeriesCalculation dateCalculation) {
double[] points = new double[dateCalculation.calculatePosition(startDate, endDate) + 1];
Arrays.fill(points, Double.NaN);
values.forEach(pt -> points[dateCalculation.calculatePosition(startDate, pt.getDate())] = pt.getValue());
return new DenseLocalDateDoubleTimeSeries(startDate, points, dateCalculation, true);
}
// Private constructor, the trusted flag indicates whether the
// points array should be cloned. If trusted, it will not be cloned.
private DenseLocalDateDoubleTimeSeries(
LocalDate startDate,
double[] points,
DenseTimeSeriesCalculation dateCalculation,
boolean trusted) {
ArgChecker.notNull(points, "points");
this.startDate = ArgChecker.notNull(startDate, "startDate");
this.points = trusted ? points : points.clone();
this.dateCalculation = ArgChecker.notNull(dateCalculation, "dateCalculation");
}
@ImmutableConstructor
private DenseLocalDateDoubleTimeSeries(
LocalDate startDate,
double[] points,
DenseTimeSeriesCalculation dateCalculation) {
this(startDate, points, dateCalculation, false);
}
//-------------------------------------------------------------------------
@Override
public boolean isEmpty() {
return !validIndices().findFirst().isPresent();
}
@Override
public int size() {
return (int) validIndices().count();
}
@Override
public boolean containsDate(LocalDate date) {
return get(date).isPresent();
}
@Override
public OptionalDouble get(LocalDate date) {
if (!isEmpty() && !date.isBefore(startDate) && dateCalculation.allowsDate(date)) {
int position = dateCalculation.calculatePosition(startDate, date);
if (position < points.length) {
double value = points[position];
if (isValidPoint(value)) {
return OptionalDouble.of(value);
}
}
}
return OptionalDouble.empty();
}
//-------------------------------------------------------------------------
private IntStream reversedValidIndices() {
// As there is no way of constructing an IntStream from
// n to m where n > m, we go from -n to m and then
// take the additive inverse (sigh!)
return IntStream.rangeClosed(1 - points.length, 0)
.map(i -> -i)
.filter(this::isValidIndex);
}
private LocalDate calculateDateFromPosition(int i) {
return dateCalculation.calculateDateFromPosition(startDate, i);
}
@Override
public LocalDate getLatestDate() {
return reversedValidIndices()
.mapToObj(this::calculateDateFromPosition)
.findFirst()
.orElseThrow(() -> new NoSuchElementException("Unable to return latest date, time-series is empty"));
}
@Override
public double getLatestValue() {
return reversedValidIndices()
.mapToDouble(i -> points[i])
.findFirst()
.orElseThrow(() -> new NoSuchElementException("Unable to return latest value, time-series is empty"));
}
//-------------------------------------------------------------------------
@Override
public LocalDateDoubleTimeSeries subSeries(LocalDate startInclusive, LocalDate endExclusive) {
ArgChecker.notNull(startInclusive, "startInclusive");
ArgChecker.notNull(endExclusive, "endExclusive");
if (endExclusive.isBefore(startInclusive)) {
throw new IllegalArgumentException(
"Invalid sub series, end before start: " + startInclusive + " to " + endExclusive);
}
// special case when this is empty or when the dates are the same
// or the series don't intersect
if (isEmpty() || startInclusive.equals(endExclusive) ||
!startDate.isBefore(endExclusive) ||
startInclusive.isAfter(getLatestDate())) {
return LocalDateDoubleTimeSeries.empty();
}
LocalDate resolvedStart = dateCalculation.adjustDate(Ordering.natural().max(startInclusive, startDate));
int startIndex = dateCalculation.calculatePosition(startDate, resolvedStart);
int endIndex = dateCalculation.calculatePosition(startDate, endExclusive);
return new DenseLocalDateDoubleTimeSeries(
resolvedStart,
Arrays.copyOfRange(points, Math.max(0, startIndex), Math.min(points.length, endIndex)),
dateCalculation,
true);
}
@Override
public LocalDateDoubleTimeSeries headSeries(int numPoints) {
ArgChecker.notNegative(numPoints, "numPoints");
if (numPoints == 0) {
return LocalDateDoubleTimeSeries.empty();
} else if (numPoints > size()) {
return this;
}
int endPosition = findHeadPoints(numPoints);
return new DenseLocalDateDoubleTimeSeries(startDate, Arrays.copyOf(points, endPosition), dateCalculation);
}
private int findHeadPoints(int required) {
// Take enough points that aren't NaN
// else we need the entire series
return validIndices()
.skip(required)
.findFirst()
.orElse(points.length);
}
@Override
public LocalDateDoubleTimeSeries tailSeries(int numPoints) {
ArgChecker.notNegative(numPoints, "numPoints");
if (numPoints == 0) {
return LocalDateDoubleTimeSeries.empty();
} else if (numPoints > size()) {
return this;
}
int startPoint = findTailPoints(numPoints);
return new DenseLocalDateDoubleTimeSeries(
calculateDateFromPosition(startPoint),
Arrays.copyOfRange(points, startPoint, points.length),
dateCalculation);
}
private int findTailPoints(int required) {
return reversedValidIndices()
.skip(required - 1)
.findFirst()
.orElse(0);
}
//-------------------------------------------------------------------------
@Override
public Stream<LocalDateDoublePoint> stream() {
return validIndices()
.mapToObj(i -> LocalDateDoublePoint.of(calculateDateFromPosition(i), points[i]));
}
@Override
public DoubleStream values() {
return Arrays.stream(points).filter(this::isValidPoint);
}
@Override
public Stream<LocalDate> dates() {
return validIndices()
.mapToObj(this::calculateDateFromPosition);
}
private IntStream validIndices() {
return IntStream.range(0, points.length)
.filter(this::isValidIndex);
}
private boolean isValidIndex(int i) {
return isValidPoint(points[i]);
}
//-------------------------------------------------------------------------
@Override
public LocalDateDoubleTimeSeries filter(ObjDoublePredicate<LocalDate> predicate) {
Stream<LocalDateDoublePoint> filteredPoints =
stream().filter(pt -> predicate.test(pt.getDate(), pt.getValue()));
// As we may have changed the density of the series by filtering
// go via the builder to get the best implementation
return new LocalDateDoubleTimeSeriesBuilder(filteredPoints).build();
}
@Override
public LocalDateDoubleTimeSeries mapDates(Function<? super LocalDate, ? extends LocalDate> mapper) {
List<LocalDate> dates = dates().map(mapper).collect(toImmutableList());
dates.stream().reduce(this::checkAscending);
return LocalDateDoubleTimeSeries.builder().putAll(dates, Doubles.asList(points)).build();
}
@Override
public LocalDateDoubleTimeSeries mapValues(DoubleUnaryOperator mapper) {
DoubleStream values = DoubleStream.of(points).map(d -> isValidPoint(d) ? applyMapper(mapper, d) : d);
return new DenseLocalDateDoubleTimeSeries(startDate, values.toArray(), dateCalculation, true);
}
private double applyMapper(DoubleUnaryOperator mapper, double d) {
double value = mapper.applyAsDouble(d);
if (!isValidPoint(value)) {
throw new IllegalArgumentException("Mapper must not map to NaN");
}
return value;
}
private boolean isValidPoint(double d) {
return !Double.isNaN(d);
}
@Override
public void forEach(ObjDoubleConsumer<LocalDate> action) {
validIndices().forEach(i -> action.accept(calculateDateFromPosition(i), points[i]));
}
@Override
public LocalDateDoubleTimeSeriesBuilder toBuilder() {
return new LocalDateDoubleTimeSeriesBuilder(stream());
}
//--------------------------------------------------------------------------------------------------
/**
* Checks the dates are in ascending order, throws an exception if not.
*
* @param earlier the date that should be earlier
* @param later the date that should be later
* @return the later date if it is after the earlier date, otherwise throw an exception
* @throws IllegalArgumentException if the dates are not in ascending order
*/
private LocalDate checkAscending(LocalDate earlier, LocalDate later) {
if (earlier.isBefore(later)) {
return later;
}
throw new IllegalArgumentException(
Messages.format(
"Dates must be in ascending order after calling mapDates but {} and {} are not",
earlier,
later));
}
//------------------------- AUTOGENERATED START -------------------------
///CLOVER:OFF
/**
* The meta-bean for {@code DenseLocalDateDoubleTimeSeries}.
* @return the meta-bean, not null
*/
public static DenseLocalDateDoubleTimeSeries.Meta meta() {
return DenseLocalDateDoubleTimeSeries.Meta.INSTANCE;
}
static {
JodaBeanUtils.registerMetaBean(DenseLocalDateDoubleTimeSeries.Meta.INSTANCE);
}
/**
* The serialization version id.
*/
private static final long serialVersionUID = 1L;
@Override
public DenseLocalDateDoubleTimeSeries.Meta metaBean() {
return DenseLocalDateDoubleTimeSeries.Meta.INSTANCE;
}
@Override
public <R> Property<R> property(String propertyName) {
return metaBean().<R>metaProperty(propertyName).createProperty(this);
}
@Override
public Set<String> propertyNames() {
return metaBean().metaPropertyMap().keySet();
}
//-----------------------------------------------------------------------
/**
* Gets date corresponding to first element in the array. All other
* values can be calculated using date arithmetic to find
* correct point.
* @return the value of the property, not null
*/
public LocalDate getStartDate() {
return startDate;
}
//-----------------------------------------------------------------------
/**
* Gets the values in the series.
* The date for each value is calculated using the position
* in the array and the start date.
* @return the value of the property, not null
*/
private double[] getPoints() {
return points.clone();
}
//-----------------------------------------------------------------------
/**
* Gets whether we should store data for the weekends (NaN will be stored
* if no data is available).
* @return the value of the property, not null
*/
private DenseTimeSeriesCalculation getDateCalculation() {
return dateCalculation;
}
//-----------------------------------------------------------------------
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj.getClass() == this.getClass()) {
DenseLocalDateDoubleTimeSeries other = (DenseLocalDateDoubleTimeSeries) obj;
return JodaBeanUtils.equal(startDate, other.startDate) &&
JodaBeanUtils.equal(points, other.points) &&
JodaBeanUtils.equal(dateCalculation, other.dateCalculation);
}
return false;
}
@Override
public int hashCode() {
int hash = getClass().hashCode();
hash = hash * 31 + JodaBeanUtils.hashCode(startDate);
hash = hash * 31 + JodaBeanUtils.hashCode(points);
hash = hash * 31 + JodaBeanUtils.hashCode(dateCalculation);
return hash;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder(128);
buf.append("DenseLocalDateDoubleTimeSeries{");
buf.append("startDate").append('=').append(startDate).append(',').append(' ');
buf.append("points").append('=').append(points).append(',').append(' ');
buf.append("dateCalculation").append('=').append(JodaBeanUtils.toString(dateCalculation));
buf.append('}');
return buf.toString();
}
//-----------------------------------------------------------------------
/**
* The meta-bean for {@code DenseLocalDateDoubleTimeSeries}.
*/
static final class Meta extends DirectMetaBean {
/**
* The singleton instance of the meta-bean.
*/
static final Meta INSTANCE = new Meta();
/**
* The meta-property for the {@code startDate} property.
*/
private final MetaProperty<LocalDate> startDate = DirectMetaProperty.ofImmutable(
this, "startDate", DenseLocalDateDoubleTimeSeries.class, LocalDate.class);
/**
* The meta-property for the {@code points} property.
*/
private final MetaProperty<double[]> points = DirectMetaProperty.ofImmutable(
this, "points", DenseLocalDateDoubleTimeSeries.class, double[].class);
/**
* The meta-property for the {@code dateCalculation} property.
*/
private final MetaProperty<DenseTimeSeriesCalculation> dateCalculation = DirectMetaProperty.ofImmutable(
this, "dateCalculation", DenseLocalDateDoubleTimeSeries.class, DenseTimeSeriesCalculation.class);
/**
* The meta-properties.
*/
private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap(
this, null,
"startDate",
"points",
"dateCalculation");
/**
* Restricted constructor.
*/
private Meta() {
}
@Override
protected MetaProperty<?> metaPropertyGet(String propertyName) {
switch (propertyName.hashCode()) {
case -2129778896: // startDate
return startDate;
case -982754077: // points
return points;
case -152592837: // dateCalculation
return dateCalculation;
}
return super.metaPropertyGet(propertyName);
}
@Override
public BeanBuilder<? extends DenseLocalDateDoubleTimeSeries> builder() {
return new DenseLocalDateDoubleTimeSeries.Builder();
}
@Override
public Class<? extends DenseLocalDateDoubleTimeSeries> beanType() {
return DenseLocalDateDoubleTimeSeries.class;
}
@Override
public Map<String, MetaProperty<?>> metaPropertyMap() {
return metaPropertyMap$;
}
//-----------------------------------------------------------------------
/**
* The meta-property for the {@code startDate} property.
* @return the meta-property, not null
*/
public MetaProperty<LocalDate> startDate() {
return startDate;
}
/**
* The meta-property for the {@code points} property.
* @return the meta-property, not null
*/
public MetaProperty<double[]> points() {
return points;
}
/**
* The meta-property for the {@code dateCalculation} property.
* @return the meta-property, not null
*/
public MetaProperty<DenseTimeSeriesCalculation> dateCalculation() {
return dateCalculation;
}
//-----------------------------------------------------------------------
@Override
protected Object propertyGet(Bean bean, String propertyName, boolean quiet) {
switch (propertyName.hashCode()) {
case -2129778896: // startDate
return ((DenseLocalDateDoubleTimeSeries) bean).getStartDate();
case -982754077: // points
return ((DenseLocalDateDoubleTimeSeries) bean).getPoints();
case -152592837: // dateCalculation
return ((DenseLocalDateDoubleTimeSeries) bean).getDateCalculation();
}
return super.propertyGet(bean, propertyName, quiet);
}
@Override
protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) {
metaProperty(propertyName);
if (quiet) {
return;
}
throw new UnsupportedOperationException("Property cannot be written: " + propertyName);
}
}
//-----------------------------------------------------------------------
/**
* The bean-builder for {@code DenseLocalDateDoubleTimeSeries}.
*/
private static final class Builder extends DirectFieldsBeanBuilder<DenseLocalDateDoubleTimeSeries> {
private LocalDate startDate;
private double[] points;
private DenseTimeSeriesCalculation dateCalculation;
/**
* Restricted constructor.
*/
private Builder() {
}
//-----------------------------------------------------------------------
@Override
public Object get(String propertyName) {
switch (propertyName.hashCode()) {
case -2129778896: // startDate
return startDate;
case -982754077: // points
return points;
case -152592837: // dateCalculation
return dateCalculation;
default:
throw new NoSuchElementException("Unknown property: " + propertyName);
}
}
@Override
public Builder set(String propertyName, Object newValue) {
switch (propertyName.hashCode()) {
case -2129778896: // startDate
this.startDate = (LocalDate) newValue;
break;
case -982754077: // points
this.points = (double[]) newValue;
break;
case -152592837: // dateCalculation
this.dateCalculation = (DenseTimeSeriesCalculation) newValue;
break;
default:
throw new NoSuchElementException("Unknown property: " + propertyName);
}
return this;
}
@Override
public Builder set(MetaProperty<?> property, Object value) {
super.set(property, value);
return this;
}
@Override
public Builder setString(String propertyName, String value) {
setString(meta().metaProperty(propertyName), value);
return this;
}
@Override
public Builder setString(MetaProperty<?> property, String value) {
super.setString(property, value);
return this;
}
@Override
public Builder setAll(Map<String, ? extends Object> propertyValueMap) {
super.setAll(propertyValueMap);
return this;
}
@Override
public DenseLocalDateDoubleTimeSeries build() {
return new DenseLocalDateDoubleTimeSeries(
startDate,
points,
dateCalculation);
}
//-----------------------------------------------------------------------
@Override
public String toString() {
StringBuilder buf = new StringBuilder(128);
buf.append("DenseLocalDateDoubleTimeSeries.Builder{");
buf.append("startDate").append('=').append(JodaBeanUtils.toString(startDate)).append(',').append(' ');
buf.append("points").append('=').append(JodaBeanUtils.toString(points)).append(',').append(' ');
buf.append("dateCalculation").append('=').append(JodaBeanUtils.toString(dateCalculation));
buf.append('}');
return buf.toString();
}
}
///CLOVER:ON
//-------------------------- AUTOGENERATED END --------------------------
}