001/*
002 * VM-Operator
003 * Copyright (C) 2023 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.vmoperator.vmconlet;
020
021import java.time.Duration;
022import java.time.Instant;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.LinkedList;
026import java.util.List;
027
028/**
029 * The Class TimeSeries.
030 */
031@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
032public class TimeSeries {
033
034    @SuppressWarnings("PMD.LooseCoupling")
035    private final LinkedList<Entry> data = new LinkedList<>();
036    private final Duration period;
037
038    /**
039     * Instantiates a new time series.
040     *
041     * @param period the period
042     */
043    public TimeSeries(Duration period) {
044        this.period = period;
045    }
046
047    /**
048     * Adds data to the series.
049     *
050     * @param time the time
051     * @param numbers the numbers
052     * @return the time series
053     */
054    @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
055        "PMD.AvoidSynchronizedStatement" })
056    public TimeSeries add(Instant time, Number... numbers) {
057        var newEntry = new Entry(time, numbers);
058        boolean nothingNew = false;
059        synchronized (data) {
060            if (data.size() >= 2) {
061                var lastEntry = data.get(data.size() - 1);
062                var lastButOneEntry = data.get(data.size() - 2);
063                nothingNew = lastEntry.valuesEqual(lastButOneEntry)
064                    && lastEntry.valuesEqual(newEntry);
065            }
066            if (nothingNew) {
067                data.removeLast();
068            }
069            data.add(new Entry(time, numbers));
070
071            // Purge
072            Instant limit = time.minus(period);
073            while (data.size() > 2
074                && data.get(0).getTime().isBefore(limit)
075                && data.get(1).getTime().isBefore(limit)) {
076                data.removeFirst();
077            }
078        }
079        return this;
080    }
081
082    /**
083     * Returns the entries.
084     *
085     * @return the list
086     */
087    @SuppressWarnings("PMD.AvoidSynchronizedStatement")
088    public List<Entry> entries() {
089        synchronized (data) {
090            return new ArrayList<>(data);
091        }
092    }
093
094    /**
095     * The Class Entry.
096     */
097    public static class Entry {
098        private final Instant timestamp;
099        private final Number[] values;
100
101        /**
102         * Instantiates a new entry.
103         *
104         * @param time the time
105         * @param numbers the numbers
106         */
107        @SuppressWarnings("PMD.ArrayIsStoredDirectly")
108        public Entry(Instant time, Number... numbers) {
109            timestamp = time;
110            values = numbers;
111        }
112
113        /**
114         * Returns the entry's time.
115         *
116         * @return the instant
117         */
118        public Instant getTime() {
119            return timestamp;
120        }
121
122        /**
123         * Returns the values.
124         *
125         * @return the number[]
126         */
127        @SuppressWarnings("PMD.MethodReturnsInternalArray")
128        public Number[] getValues() {
129            return values;
130        }
131
132        /**
133         * Returns `true` if both entries have the same values.
134         *
135         * @param other the other
136         * @return true, if successful
137         */
138        public boolean valuesEqual(Entry other) {
139            return Arrays.equals(values, other.values);
140        }
141    }
142}