gt:             for (final K key : state.keySet()) {
context: package com.github.jonfreedman.timeseries;
  
  import com.github.jonfreedman.timeseries.calculation.NonTemporalCalculator;
  import com.github.jonfreedman.timeseries.calculation.RelativeToNonTemporalCalculator;
  import com.github.jonfreedman.timeseries.calculation.RelativeToTemporalCalculator;
  import com.github.jonfreedman.timeseries.calculation.TemporalCalculator;
  import com.github.jonfreedman.timeseries.interpolator.ValueInterpolator;
  
  import java.lang.reflect.Array;
  import java.util.*;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentMap;
  import java.util.function.BiFunction;
  import java.util.function.Function;
  import java.util.function.Predicate;
  import java.util.stream.Collectors;
  
  /**
   * Immutable collection of timeseries constructed using {@link com.github.jonfreedman.timeseries.ArrayTimeSeriesCollection.Builder}
   * instance, a value will be interpolated for all time values but the interpolation can produce nulls.
   *
   * @author jon
   */
  public final class ArrayTimeSeriesCollection<K extends Comparable<K>, T extends Comparable<? super T>, V> implements TimeSeriesCollection<K, T, V> {
      private final ConcurrentMap<Integer, T> timeLookup = new ConcurrentHashMap<>();
      private final Function<T, Traverser<T>> traverserFactory;
      private SortedSet<K> keys;
      private final Map<K, V[]> values;
      private final T initialTimeValue;
      private final int maxIndex;
  
      private ArrayTimeSeriesCollection(final T initialTimeValue, final Function<T, Traverser<T>> traverserFactory,
                                        final Map<K, V[]> values) {
          this.initialTimeValue = initialTimeValue;
          this.traverserFactory = traverserFactory;
          this.keys = Collections.unmodifiableSortedSet(new TreeSet<>(values.keySet()));
          this.values = values;
  
          int max = 0;
          for (final V[] vs : values.values()) {
              max = Math.max(max, vs.length - 1);
          }
          maxIndex = max;
      }
  
      @Override
      public SortedSet<K> keySet() {
          return keys;
      }
  
      @Override
      public ArrayTimeSeriesCollection<K, T, V> filter(final Predicate<K> filter) {
          final Map<K, V[]> filteredValues = values.entrySet().stream().filter(e -> filter.test(e.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
          if (filteredValues.keySet().isEmpty()) {
              throw new IllegalArgumentException("Cannot apply filter as no keys matched");
          }
          if (filteredValues.keySet().equals(keys)) {
              return this;
          }
          return new ArrayTimeSeriesCollection<>(initialTimeValue, traverserFactory, filteredValues);
      }
  
      @Override
      public <KNew extends Comparable<KNew>> ArrayTimeSeriesCollection<KNew, T, V> group(final Function<K, KNew> groupFunction, final BiFunction<V, V, V> collationFunction) {
          final Map<KNew, V[]> grouped = new HashMap<>();
          for (final Map.Entry<K, V[]> entry : this.values.entrySet()) {
              final KNew newKey = groupFunction.apply(entry.getKey());
              //noinspection unchecked
              final V[] values = grouped.computeIfAbsent(newKey, k -> (V[]) Array.newInstance(entry.getValue()[0].getClass(), maxIndex + 1));
              for (int i = 0; i < entry.getValue().length; ++i) {
                  final V prev = values[i];
                  final V next = entry.getValue()[i];
                  values[i] = prev == null ? next : collationFunction.apply(prev, next);
              }
          }
          return new ArrayTimeSeriesCollection<>(initialTimeValue, traverserFactory, grouped);
      }
  
      @Override
      public T minValue() {
          return initialTimeValue;
      }
  
      @Override
      public T maxValue() {
          return getTimeValue(maxIndex);
      }
  
      @Override
      public int length() {
          return maxIndex + 1;
      }
  
      @Override
      public TimeSeries<T, V> get(K key) {
          return new TimeSeries<T, V>() {
              private final V[] values = ArrayTimeSeriesCollection.this.values.get(key);
  
              @Override
              public Iterator<Map.Entry<T, V>> timeValueIterator() {
                  return new Iterator<Map.Entry<T, V>>() {
                      private volatile int index = 0;
  
                      @Override
                      public boolean hasNext() {
                          return index < values.length;
                      }
  
                      @Override
                      public Map.Entry<T, V> next() {
                          final int i = index++;
                          return new Map.Entry<T, V>() {
                              @Override
                              public T getKey() {
                                  return getTimeValue(i);
                              }
  
                              @Override
                              public V getValue() {
                                  return values[i];
                              }
  
                              @Override
                              public V setValue(V value) {
                                  throw new UnsupportedOperationException("TimeSeries is immutable");
                              }
  
                              @Override
                              public String toString() {
                                  return String.format("%s -> %s", getKey(), getValue());
                              }
                          };
                      }
                  };
              }
  
              @Override
              public Iterator<V> ordinalIterator() {
                  return new Iterator<V>() {
                      private volatile int index = 0;
  
                      @Override
                      public boolean hasNext() {
                          return index < values.length;
                      }
  
                      @Override
                      public V next() {
                          return values[index++];
                      }
                  };
              }
          };
      }
  
      @Override
      public Iterator<Map.Entry<K, TimeSeries<T, V>>> iterator() {
          return new Iterator<Map.Entry<K, TimeSeries<T, V>>>() {
              private final Iterator<K> keys = ArrayTimeSeriesCollection.this.keys.iterator();
  
              @Override
              public boolean hasNext() {
                  return keys.hasNext();
              }
  
              @Override
              public Map.Entry<K, TimeSeries<T, V>> next() {
                  final K key = keys.next();
                  final TimeSeries<T, V> value = get(key);
                  return new Map.Entry<K, TimeSeries<T, V>>() {
                      @Override
                      public K getKey() {
                          return key;
                      }
  
                      @Override
                      public TimeSeries<T, V> getValue() {
                          return value;
                      }
  
                      @Override
                      public TimeSeries<T, V> setValue(TimeSeries<T, V> value) {
                          throw new UnsupportedOperationException("TimeSeriesCollection is immutable");
                      }
                  };
              }
          };
      }
  
      @SuppressWarnings("unchecked")
      @Override
      public void calculate(Collection<NonTemporalCalculator<K, ? super V, ?>> nonTemporalCalculators, Collection<TemporalCalculator<K, T, ? super V, ?>> temporalCalculators) {
          // find any relative calculators
          final List<RelativeToNonTemporalCalculator<K, ? super V, ?>> relativeNonTemporal =
                  nonTemporalCalculators.stream()
                          .filter(RelativeToNonTemporalCalculator.class::isInstance)
                          .map(c -> (RelativeToNonTemporalCalculator<K, ? super V, ?>) c)
                          .collect(Collectors.toList());
          final List<RelativeToTemporalCalculator<K, T, ? super V, ?>> relativeTemporal =
                  temporalCalculators.stream()
                          .filter(RelativeToTemporalCalculator.class::isInstance)
                          .map(c -> (RelativeToTemporalCalculator<K, T, ? super V, ?>) c)
                          .collect(Collectors.toList());
  
          // traverse entire collection
          for (int i = 0; i <= maxIndex; ++i) {
              final int currIndex = i;
              for (final K key : keys) {
                  for (final NonTemporalCalculator<K, ? super V, ?> calc : nonTemporalCalculators) {
                      calc.observation(key, values.get(key)[currIndex]);
                  }
                  for (final TemporalCalculator<K, T, ? super V, ?> calc : temporalCalculators) {
                      calc.observation(key, () -> getTimeValue(currIndex), values.get(key)[currIndex]);
                  }
              }
  
              // increment relative calculators
              for (final RelativeToNonTemporalCalculator<K, ? super V, ?> calc : relativeNonTemporal) {
                  calc.incrementRelative();
              }
              for (final RelativeToTemporalCalculator<K, T, ? super V, ?> calc : relativeTemporal) {
                  calc.incrementRelative(getTimeValue(currIndex));
              }
          }
      }
  
      private T getTimeValue(final int i) {
          return getWithPutIfAbsent(timeLookup, i, (v) -> {
              final Traverser<T> traverser = traverserFactory.apply(initialTimeValue);
              return traverser.skip(i - 1);
          });
      }
  
      private static <K, V> V getWithPutIfAbsent(final ConcurrentMap<K, V> map, final K key, final Function<Void, V> func) {
          V val = map.get(key);
          if (val == null) {
              final V constructed = func.apply(null);
              val = map.putIfAbsent(key, constructed);
              if (val == null) {
                  val = constructed;
              }
          }
          return val;
      }
  
      /**
       * As TimeSeriesCollection instances are immutable use a Builder to load data and construct an instance
       */
      public static final class Builder<K extends Comparable<K>, T extends Comparable<? super T>, V> {
          private final ConcurrentMap<K, ConcurrentMap<T, V>> state = new ConcurrentHashMap<>();
          private final BiFunction<V, V, V> collationFunction;
  
          public Builder() {
              this((t, t2) -> {
                  throw new UnsupportedOperationException();
              });
          }
  
          /**
           * @param collationFunction Function used when more than one observation provided for a given key and time value
           */
          public Builder(final BiFunction<V, V, V> collationFunction) {
              this.collationFunction = collationFunction;
          }
  
          public Builder<K, T, V> addValue(final K key, final T x, final V y) {
              if (key == null) throw new NullPointerException("key value cannot be null");
              if (x == null) throw new NullPointerException("x value cannot be null");
              if (y == null) throw new NullPointerException("y value cannot be null");
              final ConcurrentMap<T, V> values = getWithPutIfAbsent(state, key, (v) -> new ConcurrentHashMap<>());
              values.compute(x, (t, v) -> v == null ? y : collationFunction.apply(v, y));
              return this;
          }
  
          public ArrayTimeSeriesCollection<K, T, V> build(final ValueInterpolator<V> interpolator,
                                                          final Function<T, Traverser<T>> traverserFactory) {
              if (interpolator == null) throw new NullPointerException("interpolator cannot be null");
              if (traverserFactory == null) throw new NullPointerException("traverserFactory cannot be null");
  
              // build set of all time values present across all keys
              final SortedSet<T> timeValues = new TreeSet<>();
              for (final Map<T, V> vs : state.values()) {
                  timeValues.addAll(vs.keySet());
              }
              if (timeValues.isEmpty()) {
                  throw new IllegalStateException("Cannot construct an empty TimeSeriesCollection");
              }
              final Set<T> originalTimeValues = new TreeSet<>(timeValues);
              final T minTimeValue = timeValues.first();
              final T maxTimeValue = timeValues.last();
  
              // prepare integer indexed data for interpolation
              final Traverser<T> traverser = traverserFactory.apply(minTimeValue);
              if (!traverser.canStartFrom(minTimeValue)) {
                  throw new IllegalArgumentException("Cannot start with time value " + minTimeValue);
              }
  
              T timeValue = minTimeValue;
              int x = 0;
              final Map<K, SortedMap<Integer, V>> values = new TreeMap<>();
  