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    public TimeSeries add(Instant time, Number... numbers) {
056        var newEntry = new Entry(time, numbers);
057        boolean nothingNew = false;
058        synchronized (data) {
059            if (data.size() >= 2) {
060                var lastEntry = data.get(data.size() - 1);
061                var lastButOneEntry = data.get(data.size() - 2);
062                nothingNew = lastEntry.valuesEqual(lastButOneEntry)
063                    && lastEntry.valuesEqual(newEntry);
064            }
065            if (nothingNew) {
066                data.removeLast();
067            }
068            data.add(new Entry(time, numbers));
069
070            // Purge
071            Instant limit = time.minus(period);
072            while (data.size() > 2
073                && data.get(0).getTime().isBefore(limit)
074                && data.get(1).getTime().isBefore(limit)) {
075                data.removeFirst();
076            }
077        }
078        return this;
079    }
080
081    /**
082     * Returns the entries.
083     *
084     * @return the list
085     */
086    public List<Entry> entries() {
087        synchronized (data) {
088            return new ArrayList<>(data);
089        }
090    }
091
092    /**
093     * The Class Entry.
094     */
095    public static class Entry {
096        private final Instant timestamp;
097        private final Number[] values;
098
099        /**
100         * Instantiates a new entry.
101         *
102         * @param time the time
103         * @param numbers the numbers
104         */
105        @SuppressWarnings("PMD.ArrayIsStoredDirectly")
106        public Entry(Instant time, Number... numbers) {
107            timestamp = time;
108            values = numbers;
109        }
110
111        /**
112         * Returns the entry's time.
113         *
114         * @return the instant
115         */
116        public Instant getTime() {
117            return timestamp;
118        }
119
120        /**
121         * Returns the values.
122         *
123         * @return the number[]
124         */
125        @SuppressWarnings("PMD.MethodReturnsInternalArray")
126        public Number[] getValues() {
127            return values;
128        }
129
130        /**
131         * Returns `true` if both entries have the same values.
132         *
133         * @param other the other
134         * @return true, if successful
135         */
136        public boolean valuesEqual(Entry other) {
137            return Arrays.equals(values, other.values);
138        }
139    }
140}