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}