/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.collect.timeseries; import java.io.Serializable; import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; 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.Collectors; 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.primitives.Doubles; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.Messages; import com.opengamma.strata.collect.function.ObjDoublePredicate; /** * A immutable implementation of {@code DoubleTimeSeries} where the * data stored is expected to be relatively sparse. * <p> * A sparse time-series has a relatively low density of dates with values. * For example, a few points spread throughout a year. * If more or less continuous data is being used then {@link DenseLocalDateDoubleTimeSeries} * is likely to be a better choice for the data. * <p> * This implementation uses arrays internally. */ @BeanDefinition(builderScope = "private", metaScope = "package") final class SparseLocalDateDoubleTimeSeries implements ImmutableBean, Serializable, LocalDateDoubleTimeSeries { /** * An empty time-series. */ static final LocalDateDoubleTimeSeries EMPTY = new SparseLocalDateDoubleTimeSeries(new LocalDate[0], new double[0]); /** * The dates in the series. * The dates are ordered from earliest to latest. */ @PropertyDefinition(get = "manual", validate = "notNull") private final LocalDate[] dates; /** * The values in the series. * The date for each value is at the matching array index. */ @PropertyDefinition(get = "manual", validate = "notNull") private final double[] values; //------------------------------------------------------------------------- /** * Obtains a time-series from matching arrays of dates and values. * <p> * The two arrays must be the same size and must be sorted from earliest to latest. * * @param dates the date list * @param values the value list * @return the time-series */ static SparseLocalDateDoubleTimeSeries of(Collection<LocalDate> dates, Collection<Double> values) { ArgChecker.noNulls(dates, "dates"); ArgChecker.noNulls(values, "values"); LocalDate[] datesArray = dates.toArray(new LocalDate[dates.size()]); double[] valuesArray = Doubles.toArray(values); validate(datesArray, valuesArray); return createUnsafe(datesArray, valuesArray); } // creates time-series by directly assigning the input arrays // must only be called when safe to do so private static SparseLocalDateDoubleTimeSeries createUnsafe(LocalDate[] dates, double[] values) { return new SparseLocalDateDoubleTimeSeries(dates, values, true); } // validates the arrays are same length and in order private static void validate(LocalDate[] dates, double[] values) { ArgChecker.isTrue(dates.length == values.length, "Arrays are of different sizes - dates: {}, values: {}", dates.length, values.length); LocalDate maxDate = LocalDate.MIN; for (LocalDate date : dates) { ArgChecker.isTrue(date.isAfter(maxDate), "Dates must be in ascending order but: {} is not after: {}", date, maxDate); maxDate = date; } } //------------------------------------------------------------------------- /** * Creates an instance, validating the supplied arrays. * <p> * The arrays are cloned as this constructor is called from Joda-Beans. * * @param dates the dates * @param values the values */ @ImmutableConstructor private SparseLocalDateDoubleTimeSeries(LocalDate[] dates, double[] values) { ArgChecker.noNulls(dates, "dates"); ArgChecker.notNull(values, "values"); validate(dates, values); this.dates = dates.clone(); this.values = values.clone(); } /** * Creates an instance without validating the supplied arrays. * * @param dates the dates * @param values the values * @param trusted flag to distinguish constructor */ private SparseLocalDateDoubleTimeSeries(LocalDate[] dates, double[] values, boolean trusted) { // constructor exists to avoid clones where possible // because Joda-Beans owns the main constructor, this one has a weird flag // use createUnsafe() instead of calling this directly this.dates = dates; this.values = values; } //----------------------------------------------------------------------- /** * Gets the dates in the series. * The dates are ordered from earliest to latest. * @return the value of the property, not null */ private LocalDate[] getDates() { return dates.clone(); } /** * Gets the values in the series. * The date for each value is at the matching array index. * @return the value of the property, not null */ private double[] getValues() { return values.clone(); } //------------------------------------------------------------------------- @Override public int size() { return dates.length; } @Override public boolean isEmpty() { return dates.length == 0; } @Override public boolean containsDate(LocalDate date) { return (findDatePosition(date) >= 0); } @Override public OptionalDouble get(LocalDate date) { int position = findDatePosition(date); return (position >= 0 ? OptionalDouble.of(values[position]) : OptionalDouble.empty()); } private int findDatePosition(LocalDate date) { return Arrays.binarySearch(dates, date); } //------------------------------------------------------------------------- @Override public LocalDate getLatestDate() { if (isEmpty()) { throw new NoSuchElementException("Unable to return latest, time-series is empty"); } return dates[dates.length - 1]; } @Override public double getLatestValue() { if (isEmpty()) { throw new NoSuchElementException("Unable to return latest, time-series is empty"); } return values[values.length - 1]; } //------------------------------------------------------------------------- @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 if (isEmpty() || startInclusive.equals(endExclusive)) { return EMPTY; } // where in the array would start/end be (whether or not it's actually in the series) int startPos = Arrays.binarySearch(dates, startInclusive); startPos = startPos >= 0 ? startPos : -startPos - 1; int endPos = Arrays.binarySearch(dates, endExclusive); endPos = endPos >= 0 ? endPos : -endPos - 1; // create sub-series LocalDate[] timesArray = Arrays.copyOfRange(dates, startPos, endPos); double[] valuesArray = Arrays.copyOfRange(values, startPos, endPos); return createUnsafe(timesArray, valuesArray); } @Override public LocalDateDoubleTimeSeries headSeries(int numPoints) { ArgChecker.notNegative(numPoints, "numPoints"); if (numPoints == 0) { return EMPTY; } else if (numPoints >= size()) { return this; } LocalDate[] datesArray = Arrays.copyOfRange(dates, 0, numPoints); double[] valuesArray = Arrays.copyOfRange(values, 0, numPoints); return createUnsafe(datesArray, valuesArray); } @Override public LocalDateDoubleTimeSeries tailSeries(int numPoints) { ArgChecker.notNegative(numPoints, "numPoints"); if (numPoints == 0) { return EMPTY; } else if (numPoints >= size()) { return this; } LocalDate[] datesArray = Arrays.copyOfRange(dates, size() - numPoints, size()); double[] valuesArray = Arrays.copyOfRange(values, size() - numPoints, size()); return createUnsafe(datesArray, valuesArray); } //------------------------------------------------------------------------- @Override public Stream<LocalDateDoublePoint> stream() { return IntStream.range(0, size()).mapToObj(i -> LocalDateDoublePoint.of(dates[i], values[i])); } @Override public Stream<LocalDate> dates() { return Stream.of(dates); } @Override public DoubleStream values() { return DoubleStream.of(values); } //------------------------------------------------------------------------- @Override public void forEach(ObjDoubleConsumer<LocalDate> action) { ArgChecker.notNull(action, "action"); for (int i = 0; i < size(); i++) { action.accept(dates[i], values[i]); } } @Override public LocalDateDoubleTimeSeries mapDates(Function<? super LocalDate, ? extends LocalDate> mapper) { ArgChecker.notNull(mapper, "mapper"); LocalDate[] dates = Arrays.stream(this.dates).map(mapper).toArray(size -> new LocalDate[size]); // Check the dates are still in ascending order after the mapping Arrays.stream(dates).reduce(this::checkAscending); return createUnsafe(dates, values); } @Override public LocalDateDoubleTimeSeries mapValues(DoubleUnaryOperator mapper) { ArgChecker.notNull(mapper, "mapper"); return createUnsafe(dates, DoubleStream.of(values).map(mapper).toArray()); } @Override public LocalDateDoubleTimeSeries filter(ObjDoublePredicate<LocalDate> predicate) { ArgChecker.notNull(predicate, "predicate"); // build up result in arrays keeping track of count of retained dates LocalDate[] resDates = new LocalDate[size()]; double[] resValues = new double[size()]; int resCount = 0; for (int i = 0; i < size(); i++) { if (predicate.test(dates[i], values[i])) { resDates[resCount] = dates[i]; resValues[resCount] = values[i]; resCount++; } } return createUnsafe(Arrays.copyOf(resDates, resCount), Arrays.copyOf(resValues, resCount)); } //------------------------------------------------------------------------- @Override public LocalDateDoubleTimeSeriesBuilder toBuilder() { return new LocalDateDoubleTimeSeriesBuilder(dates, values); } //------------------------------------------------------------------------- /** * Checks if this time-series is equal to another time-series. * <p> * Compares this {@code LocalDateDoubleTimeSeries} with another ensuring * that the dates and values are the same. * * @param obj the object to check, null returns false * @return true if this is equal to the other date */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof SparseLocalDateDoubleTimeSeries) { SparseLocalDateDoubleTimeSeries other = (SparseLocalDateDoubleTimeSeries) obj; return Arrays.equals(dates, other.dates) && Arrays.equals(values, other.values); } return false; } /** * A hash code for this time-series. * * @return a suitable hash code */ @Override public int hashCode() { return 31 * Arrays.hashCode(dates) + Arrays.hashCode(values); } /** * Returns a string representation of the time-series. * * @return the string */ @Override public String toString() { return stream() .map(LocalDateDoublePoint::toString) .collect(Collectors.joining(", ", "[", "]")); } //-------------------------------------------------------------------------------------------------- /** * 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 SparseLocalDateDoubleTimeSeries}. * @return the meta-bean, not null */ public static SparseLocalDateDoubleTimeSeries.Meta meta() { return SparseLocalDateDoubleTimeSeries.Meta.INSTANCE; } static { JodaBeanUtils.registerMetaBean(SparseLocalDateDoubleTimeSeries.Meta.INSTANCE); } /** * The serialization version id. */ private static final long serialVersionUID = 1L; @Override public SparseLocalDateDoubleTimeSeries.Meta metaBean() { return SparseLocalDateDoubleTimeSeries.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(); } //----------------------------------------------------------------------- /** * The meta-bean for {@code SparseLocalDateDoubleTimeSeries}. */ 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 dates} property. */ private final MetaProperty<LocalDate[]> dates = DirectMetaProperty.ofImmutable( this, "dates", SparseLocalDateDoubleTimeSeries.class, LocalDate[].class); /** * The meta-property for the {@code values} property. */ private final MetaProperty<double[]> values = DirectMetaProperty.ofImmutable( this, "values", SparseLocalDateDoubleTimeSeries.class, double[].class); /** * The meta-properties. */ private final Map<String, MetaProperty<?>> metaPropertyMap$ = new DirectMetaPropertyMap( this, null, "dates", "values"); /** * Restricted constructor. */ private Meta() { } @Override protected MetaProperty<?> metaPropertyGet(String propertyName) { switch (propertyName.hashCode()) { case 95356549: // dates return dates; case -823812830: // values return values; } return super.metaPropertyGet(propertyName); } @Override public BeanBuilder<? extends SparseLocalDateDoubleTimeSeries> builder() { return new SparseLocalDateDoubleTimeSeries.Builder(); } @Override public Class<? extends SparseLocalDateDoubleTimeSeries> beanType() { return SparseLocalDateDoubleTimeSeries.class; } @Override public Map<String, MetaProperty<?>> metaPropertyMap() { return metaPropertyMap$; } //----------------------------------------------------------------------- /** * The meta-property for the {@code dates} property. * @return the meta-property, not null */ public MetaProperty<LocalDate[]> dates() { return dates; } /** * The meta-property for the {@code values} property. * @return the meta-property, not null */ public MetaProperty<double[]> values() { return values; } //----------------------------------------------------------------------- @Override protected Object propertyGet(Bean bean, String propertyName, boolean quiet) { switch (propertyName.hashCode()) { case 95356549: // dates return ((SparseLocalDateDoubleTimeSeries) bean).getDates(); case -823812830: // values return ((SparseLocalDateDoubleTimeSeries) bean).getValues(); } 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 SparseLocalDateDoubleTimeSeries}. */ private static final class Builder extends DirectFieldsBeanBuilder<SparseLocalDateDoubleTimeSeries> { private LocalDate[] dates; private double[] values; /** * Restricted constructor. */ private Builder() { } //----------------------------------------------------------------------- @Override public Object get(String propertyName) { switch (propertyName.hashCode()) { case 95356549: // dates return dates; case -823812830: // values return values; default: throw new NoSuchElementException("Unknown property: " + propertyName); } } @Override public Builder set(String propertyName, Object newValue) { switch (propertyName.hashCode()) { case 95356549: // dates this.dates = (LocalDate[]) newValue; break; case -823812830: // values this.values = (double[]) 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 SparseLocalDateDoubleTimeSeries build() { return new SparseLocalDateDoubleTimeSeries( dates, values); } //----------------------------------------------------------------------- @Override public String toString() { StringBuilder buf = new StringBuilder(96); buf.append("SparseLocalDateDoubleTimeSeries.Builder{"); buf.append("dates").append('=').append(JodaBeanUtils.toString(dates)).append(',').append(' '); buf.append("values").append('=').append(JodaBeanUtils.toString(values)); buf.append('}'); return buf.toString(); } } ///CLOVER:ON //-------------------------- AUTOGENERATED END -------------------------- }