/** * 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 static com.opengamma.strata.collect.timeseries.DenseLocalDateDoubleTimeSeries.DenseTimeSeriesCalculation.INCLUDE_WEEKENDS; import static com.opengamma.strata.collect.timeseries.DenseLocalDateDoubleTimeSeries.DenseTimeSeriesCalculation.SKIP_WEEKENDS; import java.time.LocalDate; import java.time.temporal.ChronoField; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.OptionalDouble; import java.util.SortedMap; import java.util.TreeMap; import java.util.function.DoubleBinaryOperator; import java.util.stream.Stream; import com.opengamma.strata.collect.ArgChecker; /** * Builder to create the immutable {@code LocalDateDoubleTimeSeries}. * <p> * This builder allows a time-series to be created. * Entries can be added to the builder in any order. * If a date is duplicated it will overwrite an earlier entry. * <p> * Use {@link LocalDateDoubleTimeSeries#builder()} to create an instance. */ public final class LocalDateDoubleTimeSeriesBuilder { /** * Threshold for deciding whether we use the dense or sparse time-series implementation. */ private static final double DENSITY_THRESHOLD = 0.7; /** * The entries for the time-series. */ private final SortedMap<LocalDate, Double> entries = new TreeMap<>(); /** * Keep track of whether we have weekends in the data. */ private boolean containsWeekends; //------------------------------------------------------------------------- /** * Creates an instance. * <p> * Use {@link LocalDateDoubleTimeSeries#builder()}. */ LocalDateDoubleTimeSeriesBuilder() { } /** * Creates an instance. * <p> * Use {@link LocalDateDoubleTimeSeries#toBuilder()}. * * @param dates the dates to initialize with * @param values the values to initialize with */ LocalDateDoubleTimeSeriesBuilder(LocalDate[] dates, double[] values) { for (int i = 0; i < dates.length; i++) { put(dates[i], values[i]); } } /** * Creates an instance. * <p> * Use {@link DenseLocalDateDoubleTimeSeries#toBuilder()}. * * @param points the stream of points to initialize with */ LocalDateDoubleTimeSeriesBuilder(Stream<LocalDateDoublePoint> points) { points.forEach(pt -> put(pt.getDate(), pt.getValue())); } //------------------------------------------------------------------------- /** * 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 OptionalDouble get(LocalDate date) { Double value = entries.get(date); return (value != null ? OptionalDouble.of(value) : OptionalDouble.empty()); } //------------------------------------------------------------------------- /** * Puts the specified date/value point into this builder. * * @param date the date to be added * @param value the value associated with the date * @return this builder */ public LocalDateDoubleTimeSeriesBuilder put(LocalDate date, double value) { ArgChecker.notNull(date, "date"); ArgChecker.isFalse(Double.isNaN(value), "NaN is not allowed as a value"); entries.put(date, value); if (!containsWeekends && date.get(ChronoField.DAY_OF_WEEK) > 5) { containsWeekends = true; } return this; } /** * Puts the specified date/value point into this builder. * * @param point the point to be added * @return this builder */ public LocalDateDoubleTimeSeriesBuilder put(LocalDateDoublePoint point) { ArgChecker.notNull(point, "point"); put(point.getDate(), point.getValue()); return this; } //------------------------------------------------------------------------- /** * Merges the specified date/value point into this builder. * <p> * The operator is invoked if the date already exists. * * @param date the date to be added * @param value the value associated with the date * @param operator the operator to use for merging * @return this builder */ public LocalDateDoubleTimeSeriesBuilder merge(LocalDate date, double value, DoubleBinaryOperator operator) { ArgChecker.notNull(date, "date"); ArgChecker.notNull(operator, "operator"); entries.merge(date, value, (a, b) -> operator.applyAsDouble(a, b)); return this; } /** * Merges the specified date/value point into this builder. * <p> * The operator is invoked if the date already exists. * * @param point the point to be added * @param operator the operator to use for merging * @return this builder */ public LocalDateDoubleTimeSeriesBuilder merge(LocalDateDoublePoint point, DoubleBinaryOperator operator) { ArgChecker.notNull(point, "point"); entries.merge(point.getDate(), point.getValue(), (a, b) -> operator.applyAsDouble(a, b)); return this; } //------------------------------------------------------------------------- /** * Puts all the specified dates and values into this builder. * <p> * The date and value collections must be the same size. * <p> * The date-value pairs are added one by one. * If a date is duplicated it will overwrite an earlier entry. * * @param dates the dates to be added * @param values the values to be added * @return this builder */ public LocalDateDoubleTimeSeriesBuilder putAll(Collection<LocalDate> dates, Collection<Double> values) { ArgChecker.noNulls(dates, "dates"); ArgChecker.noNulls(values, "values"); ArgChecker.isTrue(dates.size() == values.size(), "Arrays are of different sizes - dates: {}, values: {}", dates.size(), values.size()); Iterator<LocalDate> itDate = dates.iterator(); Iterator<Double> itValue = values.iterator(); for (int i = 0; i < dates.size(); i++) { put(itDate.next(), itValue.next()); } return this; } /** * Puts all the specified dates and values into this builder. * <p> * The date collection and value array must be the same size. * <p> * The date-value pairs are added one by one. * If a date is duplicated it will overwrite an earlier entry. * * @param dates the dates to be added * @param values the values to be added * @return this builder */ public LocalDateDoubleTimeSeriesBuilder putAll(Collection<LocalDate> dates, double[] values) { ArgChecker.noNulls(dates, "dates"); ArgChecker.notNull(values, "values"); ArgChecker.isTrue(dates.size() == values.length, "Arrays are of different sizes - dates: {}, values: {}", dates.size(), values.length); Iterator<LocalDate> itDate = dates.iterator(); for (int i = 0; i < dates.size(); i++) { put(itDate.next(), values[i]); } return this; } /** * Puts all the specified points into this builder. * <p> * The points are added one by one. * If a date is duplicated it will overwrite an earlier entry. * * @param points the points to be added * @return this builder */ public LocalDateDoubleTimeSeriesBuilder putAll(Stream<LocalDateDoublePoint> points) { ArgChecker.notNull(points, "points"); points.forEach(this::put); return this; } /** * Puts all the specified points into this builder. * <p> * The points are added one by one. * If a date is duplicated it will overwrite an earlier entry. * * @param points the points to be added * @return this builder */ public LocalDateDoubleTimeSeriesBuilder putAll(List<LocalDateDoublePoint> points) { ArgChecker.notNull(points, "points"); return putAll(points.stream()); } /** * Puts the contents of the specified builder into this builder. * <p> * The points are added one by one. * If a date is duplicated it will overwrite an earlier entry. * * @param other the other builder * @return this builder */ public LocalDateDoubleTimeSeriesBuilder putAll(LocalDateDoubleTimeSeriesBuilder other) { ArgChecker.notNull(other, "other"); entries.putAll(other.entries); containsWeekends = containsWeekends || other.containsWeekends; return this; } /** * Puts all the entries from the supplied map into this builder. * <p> * If a date is duplicated it will overwrite an earlier entry. * * @param map the map of points to be added * @return this builder */ public LocalDateDoubleTimeSeriesBuilder putAll(Map<LocalDate, Double> map) { ArgChecker.noNulls(map, "map"); map.entrySet().forEach(e -> put(e.getKey(), e.getValue())); return this; } //------------------------------------------------------------------------- /** * Build the time-series from the builder. * * @return a time-series containing the entries from the builder */ public LocalDateDoubleTimeSeries build() { if (entries.isEmpty()) { return LocalDateDoubleTimeSeries.empty(); } // Depending on how dense the data is, judge which type of time series // is the best fit return density() > DENSITY_THRESHOLD ? createDenseSeries() : createSparseSeries(); } private LocalDateDoubleTimeSeries createDenseSeries() { return DenseLocalDateDoubleTimeSeries.of( entries.firstKey(), entries.lastKey(), streamEntries(), determineCalculation()); } private SparseLocalDateDoubleTimeSeries createSparseSeries() { return SparseLocalDateDoubleTimeSeries.of(entries.keySet(), entries.values()); } private Stream<LocalDateDoublePoint> streamEntries() { return entries.entrySet() .stream() .map(e -> LocalDateDoublePoint.of(e.getKey(), e.getValue())); } private DenseLocalDateDoubleTimeSeries.DenseTimeSeriesCalculation determineCalculation() { return containsWeekends ? INCLUDE_WEEKENDS : SKIP_WEEKENDS; } private double density() { // We can use the calculators to work out range size double rangeSize = determineCalculation().calculatePosition(entries.firstKey(), entries.lastKey()) + 1; return entries.size() / rangeSize; } }