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.vmmgmt;
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 */
031public class TimeSeries {
032
033    @SuppressWarnings("PMD.LooseCoupling")
034    private final LinkedList<Entry> data = new LinkedList<>();
035    private final Duration period;
036
037    /**
038     * Instantiates a new time series.
039     *
040     * @param period the period
041     */
042    public TimeSeries(Duration period) {
043        this.period = period;
044    }
045
046    /**
047     * Adds data to the series.
048     *
049     * @param time the time
050     * @param numbers the numbers
051     * @return the time series
052     */
053    @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
054        "PMD.AvoidSynchronizedStatement" })
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    @SuppressWarnings("PMD.AvoidSynchronizedStatement")
087    public List<Entry> entries() {
088        synchronized (data) {
089            return new ArrayList<>(data);
090        }
091    }
092
093    /**
094     * The Class Entry.
095     */
096    public static class Entry {
097        private final Instant timestamp;
098        private final Number[] values;
099
100        /**
101         * Instantiates a new entry.
102         *
103         * @param time the time
104         * @param numbers the numbers
105         */
106        @SuppressWarnings("PMD.ArrayIsStoredDirectly")
107        public Entry(Instant time, Number... numbers) {
108            timestamp = time;
109            values = numbers;
110        }
111
112        /**
113         * Returns the entry's time.
114         *
115         * @return the instant
116         */
117        public Instant getTime() {
118            return timestamp;
119        }
120
121        /**
122         * Returns the values.
123         *
124         * @return the number[]
125         */
126        @SuppressWarnings("PMD.MethodReturnsInternalArray")
127        public Number[] getValues() {
128            return values;
129        }
130
131        /**
132         * Returns `true` if both entries have the same values.
133         *
134         * @param other the other
135         * @return true, if successful
136         */
137        public boolean valuesEqual(Entry other) {
138            return Arrays.equals(values, other.values);
139        }
140    }
141}