All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.brein.time.timeseries.BucketTimeSeries Maven / Gradle / Ivy

package com.brein.time.timeseries;

import com.brein.time.exceptions.IllegalConfiguration;
import com.brein.time.exceptions.IllegalTimePoint;
import com.brein.time.exceptions.IllegalTimePointIndex;
import com.brein.time.exceptions.IllegalTimePointMovement;
import com.brein.time.exceptions.IllegalValueRegardingConfiguration;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * This implementation represents a time-series. Each time-point of the series represents several actual time-points on
 * the underlying time-axis (buckets).
 * 

*

 * The data structure (which is based on an array) can be explained best with
 * an illustration (with n == timeSeriesSize):
 *
 *   [0] [1] [2] [3] [4] [5] [6] ... [n]
 *        ↑
 *  currentNowIdx
 *
 * Each array field is a bucket of time-stamps (ordered back from now):
 *
 *   [1] ==> [1456980000, 1456980300) ← now, 1 == currentNowIdx
 *   [2] ==> [1456970700, 1456980000)
 *   ...
 *   [n] ==> ...
 *   [0] ==> ...
 * 
* * @param the content held by the time-series * * @author Philipp */ public class BucketTimeSeries implements Iterable, Serializable { private static final long serialVersionUID = 2L; protected final BucketTimeSeriesConfig config; protected T[] timeSeries = null; protected BucketEndPoints now = null; protected int currentNowIdx = -1; // the observer has to be reset every time it gets deserialized protected transient BiConsumer observer; public BucketTimeSeries(final BucketTimeSeriesConfig config) { this(config, null); } public BucketTimeSeries(final BucketTimeSeriesConfig config, final BiConsumer observer) { this.config = config; this.timeSeries = createEmptyArray(); this.observer = observer; } public void setObserver(final BiConsumer observer) { this.observer = observer; } /** * Constructor to create a pre-set time series. * * @param config the configuration to use * @param timeSeries the initiale time series * @param now the current now timestamp */ public BucketTimeSeries(final BucketTimeSeriesConfig config, final T[] timeSeries, final long now) throws IllegalValueRegardingConfiguration { this.config = config; this.timeSeries = timeSeries; this.now = normalizeUnixTimeStamp(now); this.currentNowIdx = 0; if (this.timeSeries != null && this.timeSeries.length != config.getTimeSeriesSize()) { throw new IllegalValueRegardingConfiguration("The defined size of the time-series does not satisfy the " + "configured time-series size (" + this.timeSeries.length + " vs. " + config .getTimeSeriesSize() + ")."); } } @SuppressWarnings("unchecked") protected T[] createEmptyArray() { final T[] array = (T[]) Array.newInstance(config.getBucketContent(), config.getTimeSeriesSize()); if (applyZero()) { Arrays.fill(array, 0, array.length, zero()); } return array; } protected boolean applyZero() { return config.isFillNumberWithZero() && Number.class.isAssignableFrom(config.getBucketContent()); } /** * Resets the values from [fromIndex, endIndex). * * @param fromIndex the index to start from (included) * @param endIndex the index to end (excluded) */ protected void fill(int fromIndex, int endIndex) { fromIndex = fromIndex == -1 ? 0 : fromIndex; endIndex = endIndex == -1 || endIndex > this.timeSeries.length ? this.timeSeries.length : endIndex; final T val; if (applyZero()) { val = zero(); } else { val = null; } // set the values for (int i = fromIndex; i < endIndex; i++) { set(i, val); } } public T[] getTimeSeries() { return timeSeries; } @SuppressWarnings("unchecked") public T[] order() { final T[] result; if (this.timeSeries != null && this.currentNowIdx != -1) { result = (T[]) Array.newInstance(config.getBucketContent(), config.getTimeSeriesSize()); final AtomicInteger i = new AtomicInteger(0); forEach(val -> result[i.getAndIncrement()] = val); } else { result = createEmptyArray(); } return result; } public long[] create(final Function supplier) { final long[] result = new long[config.getTimeSeriesSize()]; if (this.timeSeries != null && this.currentNowIdx != -1) { final AtomicInteger i = new AtomicInteger(0); forEach(val -> result[i.getAndIncrement()] = supplier.apply(val)); } else { final boolean applyZero = applyZero(); // just assume we have nulls everywhere for (int i = 0; i < result.length; i++) { if (applyZero) { result[i] = supplier.apply(zero()); } else { result[i] = supplier.apply(null); } } } return result; } public int getNowIdx() { return currentNowIdx; } /** * This method returns the value from the time-series as if it would be ordered (i.e., zero is now, 1 is the * previous moment, ...). * * @param idx the zero based index * * @return the value associated to the zero based index */ public T getFromZeroBasedIdx(final int idx) { if (this.timeSeries != null && this.currentNowIdx != -1) { // we can use the default validation, because the index still must be in the borders of the time-series validateIdx(idx); return get(idx(currentNowIdx + idx)); } else { return null; } } /** * Gets the end-points by an offset to now, i.e., 0 means to get the now bucket, -1 gets the previous bucket, and +1 * will get the next bucket. * * @param bucketsFromNow the amount of buckets to retrieve using now as anchor * * @return the end-point (bucket) with an offset of {@code bucketsFromNow} from now * * @throws IllegalTimePoint if the current now is not defined */ public BucketEndPoints getEndPoints(final int bucketsFromNow) throws IllegalTimePoint { if (currentNowIdx == -1 || now == null) { throw new IllegalTimePoint("The now is not set yet, thus no end-points can be returned"); } return now.move(bucketsFromNow); } /** * Gets the time-stamp representing the beginning of the specified bucket, i.e., if {@code offset} is {@code 0}, the * value returned will be the current bucket's start. * * @param offset the zero-based position, i.e., 0 is now, 1 is the first bucket before now, ... * * @return the start time-stamp of the bucket at the offset position */ public long getTimeStamp(final int offset) { return getEndPoints(-1 * offset).getUnixTimeStampStart(); } public void set(final long unixTimeStamp, final T value) { final int idx = handleDataUnixTimeStamp(unixTimeStamp); if (idx == -1) { return; } set(idx, value); } public void modify(final long unixTimeStamp, final Function mod) { modify(unixTimeStamp, (ignore, val) -> mod.apply(val)); } public void modify(final long unixTimeStamp, final BiFunction mod) { final int idx = handleDataUnixTimeStamp(unixTimeStamp); if (idx == -1) { return; } set(idx, mod.apply(idx, get(idx))); } public void modify(final int idx, final Function mod) { set(idx, mod.apply(get(idx))); } public void set(final int idx, final T value) throws IllegalTimePointIndex { validateIdx(idx); this.timeSeries[idx] = value; // call the observer on a value change if (this.observer != null) { this.observer.accept(idx, value); } } public void initNow(final long now, final int currentNowIdx) { if (this.now == null && this.currentNowIdx == -1) { this.now = normalizeUnixTimeStamp(now); this.currentNowIdx = currentNowIdx; } else { throw new IllegalTimePointMovement("Cannot modify the now and the currentIdx once values where added."); } } /** * Gets the value for the specified {@code idx}. * * @param idx the index of the bucket to get the current value for; must be a valid index * * @return the current value for selected bucket * * @see #getNowIdx() */ public T get(final int idx) { validateIdx(idx); return this.timeSeries[idx]; } /** * Determines the number of buckets used to cover the seconds. * * @param diffInSeconds the difference in seconds * * @return the amount of buckets used to cover this amount */ public int getBucketSize(final long diffInSeconds) { // convert one unit of this into seconds final long secondsPerBucket = TimeUnit.SECONDS.convert(config.getBucketSize(), config.getTimeUnit()); return (int) Math.ceil((double) diffInSeconds / secondsPerBucket); } protected int handleDataUnixTimeStamp(final long unixTimeStamp) { // first we have to determine the bucket (idx) final BucketEndPoints bucketEndPoints = normalizeUnixTimeStamp(unixTimeStamp); final long diff; if (this.now == null) { setNow(unixTimeStamp); diff = 0L; } else { diff = this.now.diff(bucketEndPoints); } // we are in the future, let's move there and set it if (diff > 0) { setNow(unixTimeStamp); return currentNowIdx; } // if we are outside the time, just ignore it else if (Math.abs(diff) >= config.getTimeSeriesSize()) { // do nothing return -1; } // the absolute index has to be made relative (idx(..)) else { return idx(currentNowIdx - diff); } } protected void validateIdx(final int idx) throws IllegalTimePointIndex { if (idx < 0 || idx >= config.getTimeSeriesSize()) { throw new IllegalTimePointIndex(String.format("The index %d is out of bound [%d, %d].", idx, 0, config .getTimeSeriesSize() - 1)); } } protected int idx(final long absIdx) { /* * The absolute index has to be mapped to a real array index. This is done * by using the modulo operation. Nevertheless, the module has a range of * -1 * config.getTimeSeriesSize() to config.getTimeSeriesSize(). Thus, * if negative we have to add the config.getTimeSeriesSize() once. */ final int idx = (int) (absIdx % config.getTimeSeriesSize()); return idx < 0 ? idx + config.getTimeSeriesSize() : idx; } /** * This method is used to determine the bucket the {@code unixTimeStamp} belongs into. The bucket is represented by * a {@link BucketEndPoints} instance, which defines the end-points of the bucket and provides methods to calculate * the distance between buckets. * * @param unixTimeStamp the time-stamp to determine the bucket for * * @return the bucket for the specified {@code unixTimeStamp} based on the configuration of the time-series */ public BucketEndPoints normalizeUnixTimeStamp(final long unixTimeStamp) { final TimeUnit timeUnit = config.getTimeUnit(); // first get the time stamp in the unit of the time-series final long timeStamp = timeUnit.convert(unixTimeStamp, TimeUnit.SECONDS); /* * Now lets, normalize the time stamp regarding to the bucketSize: * 1.) we need the size of a bucket * 2.) we need where the current time stamp is located within the size, * i.e., how much it reaches into the bucket (ratio => offset) * 3.) we use the calculated offset to determine the end points (in seconds) * * Example: * The time stamp 1002 (in minutes or seconds or ...?) should be mapped * to a normalized bucket, so the bucket would be, e.g., [1000, 1005). */ final int bucketSize = config.getBucketSize(); final long offset = timeStamp % bucketSize; final long start = timeStamp - offset; final long end = start + config.getBucketSize(); return new BucketEndPoints(TimeUnit.SECONDS.convert(start, timeUnit), TimeUnit.SECONDS.convert(end, timeUnit)); } @SuppressWarnings("unchecked") protected T zero() { final Class contentType = config.getBucketContent(); if (Number.class.isAssignableFrom(contentType)) { if (Byte.class.equals(contentType)) { return (T) Byte.valueOf((byte) 0); } else if (Short.class.equals(contentType)) { return (T) Short.valueOf((short) 0); } else if (Integer.class.equals(contentType)) { return (T) Integer.valueOf(0); } else if (Long.class.equals(contentType)) { return (T) Long.valueOf(0L); } else if (Double.class.equals(contentType)) { return (T) Double.valueOf(0.0); } else if (Float.class.equals(contentType)) { return (T) Float.valueOf(0.0f); } else if (BigDecimal.class.equals(contentType)) { return (T) BigDecimal.valueOf(0); } else if (BigInteger.class.equals(contentType)) { return (T) BigInteger.valueOf(0); } else if (AtomicInteger.class.equals(contentType)) { return (T) new AtomicInteger(0); } else if (AtomicLong.class.equals(contentType)) { return (T) new AtomicLong(0); } else { return null; } } else { return null; } } public BucketTimeSeriesConfig getConfig() { return config; } @Override @SuppressWarnings("NullableProblems") public Iterator iterator() { return new Iterator() { final int currentIdx = getNowIdx(); final int size = BucketTimeSeries.this.config.getTimeSeriesSize(); int offset = 0; @Override public boolean hasNext() { return offset < size; } @Override public T next() { if (hasNext()) { final int idx = BucketTimeSeries.this.idx((long) currentIdx + offset); offset++; return BucketTimeSeries.this.timeSeries[idx]; } else { throw new NoSuchElementException("No further elements available."); } } }; } public long getNow() { if (now == null) { return -1L; } else { return now.getUnixTimeStampEnd() - 1; } } /** * Modifies the "now" unix time stamp of the time-series. This modifies, the time-series, i.e., data might be * removed if the data is pushed. * * @param unixTimeStamp the new now to be used * * @throws IllegalTimePointMovement if the new unix time stamp it moved into the past, e.g., if the current time * stamp is newers */ public void setNow(final long unixTimeStamp) throws IllegalTimePointMovement { /* * "now" strongly depends on the TimeUnit used for the timeSeries, as * well as the bucketSize. If, e.g., the TimeUnit is MINUTES and the * bucketSize is 5, a unix time stamp representing 01/20/1981 08:07:30 * must be mapped to 01/20/1981 08:10:00 (the next valid bucket). */ if (this.currentNowIdx == -1 || this.now == null) { this.currentNowIdx = 0; this.now = normalizeUnixTimeStamp(unixTimeStamp); } else { /* * Getting the new currentNowIdx is done by calculating the * difference between the old now and the new now and moving * the currentNowIdx forward. * * [0] [1] [2] [3] [4] [5] [6] * ↑ * currentNowIdx * * Assume we move the now time stamp forward by three buckets: * * [0] [1] [2] [3] [4] [5] [6] * ↑ * currentNowIdx * * So the calculation is done in two steps: * 1.) get the bucket of the new now * 2.) determine the difference between the buckets, if it's negative => error, * if it is zero => done, otherwise => erase the fields in between and reset * to zero or null */ final BucketEndPoints newNow = normalizeUnixTimeStamp(unixTimeStamp); final long diff = this.now.diff(newNow); if (diff < 0) { throw new IllegalTimePointMovement(String.format("Cannot move to the past (current: %s, update: %s)", this.now, newNow)); } else if (diff > 0) { final int newCurrentNowIdx = idx(currentNowIdx - diff); /* * Remove the "passed" information. There are several things we have to * consider: * 1.) the whole array has to be reset * 2.) the array has to be reset partly forward * 3.) the array has to be reset "around the corner" */ if (diff >= config.getTimeSeriesSize()) { fill(-1, -1); } else if (newCurrentNowIdx > currentNowIdx) { fill(0, currentNowIdx); fill(newCurrentNowIdx, -1); } else { fill(newCurrentNowIdx, currentNowIdx); } // set the values calculated this.currentNowIdx = newCurrentNowIdx; this.now = newNow; } } } public void setTimeSeries(final T[] timeSeries, final long now) { this.now = normalizeUnixTimeStamp(now); this.currentNowIdx = 0; this.timeSeries = timeSeries; } public void combine(final BucketTimeSeries timeSeries) throws IllegalConfiguration { combine(timeSeries, this::addition); } public void combine(final BucketTimeSeries timeSeries, final BiFunction cmb) throws IllegalConfiguration { final BucketTimeSeries syncedTs = sync(timeSeries, ts -> new BucketTimeSeries<>(ts.getConfig(), ts.timeSeries, ts.getNow())); for (int i = 0; i < config.getTimeSeriesSize(); i++) { final int idx = idx(currentNowIdx + i); set(idx, cmb.apply(get(idx), syncedTs.get(syncedTs.idx(syncedTs.currentNowIdx + i)))); } } protected > B sync(final B timeSeries, final Function copy) throws IllegalConfiguration { if (!Objects.equals(timeSeries.config, config)) { throw new IllegalConfiguration("The time-series to combine must have the same configuration."); } final int cmp = Long.compare(getNow(), timeSeries.getNow()); final B ts; if (cmp != 0 && getNow() == -1) { ts = timeSeries; setNow(timeSeries.getNow()); } else if (cmp != 0 && timeSeries.getNow() == -1) { ts = timeSeries; } else { if (cmp == 0) { ts = timeSeries; } else if (cmp > 0) { // the passed time-series is in the past ts = copy.apply(timeSeries); ts.setNow(this.getNow()); } else { // the passed time-series is in the future this.setNow(timeSeries.getNow()); ts = timeSeries; } } return ts; } @Override public String toString() { return Arrays.toString(order()); } @SuppressWarnings("unchecked") public T addition(final T a, final T b) { if (a == null && b == null) { return null; } else if (a == null) { return addition(zero(), b); } else if (b == null) { return addition(a, zero()); } final Class contentType = (Class) a.getClass(); if (Number.class.isAssignableFrom(contentType)) { if (Byte.class.equals(contentType)) { return (T) Byte.valueOf(Integer.valueOf(Byte.class.cast(a) + Byte.class.cast(b)).byteValue()); } else if (Short.class.equals(contentType)) { return (T) Short.valueOf(Integer.valueOf(Short.class.cast(a) + Short.class.cast(b)).shortValue()); } else if (Integer.class.equals(contentType)) { return (T) Integer.valueOf(Integer.class.cast(a) + Integer.class.cast(b)); } else if (Long.class.equals(contentType)) { return (T) Long.valueOf(Long.class.cast(a) + Long.class.cast(b)); } else if (Double.class.equals(contentType)) { return (T) Double.valueOf(Double.class.cast(a) + Double.class.cast(b)); } else if (Float.class.equals(contentType)) { return (T) Float.valueOf(Float.class.cast(a) + Float.class.cast(b)); } else if (BigDecimal.class.equals(contentType)) { return (T) BigDecimal.class.cast(a).add(BigDecimal.class.cast(b)); } else if (BigInteger.class.equals(contentType)) { return (T) BigInteger.class.cast(a).add(BigInteger.class.cast(b)); } else if (AtomicInteger.class.equals(contentType)) { final int intA = AtomicInteger.class.cast(a).intValue(); final int intB = AtomicInteger.class.cast(b).intValue(); return (T) new AtomicInteger(intA + intB); } else if (AtomicLong.class.equals(contentType)) { final long longA = AtomicLong.class.cast(a).longValue(); final long longB = AtomicLong.class.cast(b).longValue(); return (T) new AtomicLong(longA + longB); } else { return null; } } else if (String.class.isAssignableFrom(contentType)) { return (T) (String.class.cast(a) + String.class.cast(b)); } else if (List.class.isAssignableFrom(contentType)) { final List list = new ArrayList<>(); list.addAll(List.class.cast(a)); list.addAll(List.class.cast(b)); return (T) list; } else if (Set.class.isAssignableFrom(contentType)) { final Set set = new HashSet<>(); set.addAll(Set.class.cast(a)); set.addAll(Set.class.cast(b)); return (T) set; } else { return null; } } public long sumTimeSeries() { long totalSum = 0; for (final T i : this.timeSeries) { totalSum += ((Number) i).longValue(); } return totalSum; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy