Группировка объектов по времени
Сгруппировать объекты Item из листа items в список объектов Group, каждая группа хранит данные относящиеся к одной минуте.
Данные value внутри группы должны быть сгруппированы как среднеарифметические значения для заданного интервала в секундах. Если данных для текущего интервала N в исходном листе нет, то дублируется значение предыдущего интервала. Если данных не хватает для первого интервала в группе то дублируется последний интервал предыдущей группы.
Интервал всегда меньше или равен 60 сек. и делит минуту на равное число секунд без остатка.
public class Item {
private int id;
private String time;
private double value;
// Getters, setters, constructors...
}
public class Group {
private int id;
private List<Item> items;
// Getters, setters, constructors...
}
При длительности интервале = 30 для исходного списка:
List<Item> items = List.of(
new Item(1, "19/09/2020 1:03:00 AM", 1.0),
new Item(2, "19/09/2020 1:03:03 AM", 1.3),
new Item(3, "19/09/2020 1:03:15 AM", 1.1),
new Item(4, "19/09/2020 1:03:47 AM", 1.2),
new Item(5, "19/09/2020 1:03:57 AM", 1.6),
new Item(6, "19/09/2020 1:04:04 AM", 1.8),
new Item(7, "19/09/2020 1:04:43 AM", 1.9),
new Item(8, "19/09/2020 1:04:44 AM", 2.1),
new Item(9, "19/09/2020 1:05:30 AM", 1.8),
new Item(10, "19/09/2020 1:05:46 AM", 2.3)
);
Должно получиться:
List.of(
new Group(1, List.of(
new Item(1, "19/09/2020 1:03:00 AM", 1.13), // первые 30 сек value = (1.0 + 1.3 + 1.1) / 3
new Item(2, "19/09/2020 1:03:30 AM", 1.4) // вторые 30 сек value = (1.2 + 1.6) / 2
)),
new Group(2, List.of(
new Item(1, "19/09/2020 1:04:00 AM", 1.8), // первые 30 сек
new Item(2, "19/09/2020 1:04:30 AM", 1.5) // вторые 30 сек
)),
new Group(2, List.of(
new Item(1, "19/09/2020 1:05:00 AM", 1.5), // для первых 30 сек данных нет, в результат пойдет предыдущее значение
new Item(2, "19/09/2020 1:05:30 AM", 2.05) // вторые 30 сек
)));
Сигнатура List<Group> transform(List<Item> src, int intervalSize)
Все что мне пока удалось это только создать пустой список групп
public class Transformer {
SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss a", Locale.ENGLISH);
@SneakyThrows
public List<Group> transform(List<Item> source, int intervalSize) {
List<Group> target = getEmptyGroups(source);
return target;
}
@SneakyThrows
private List<Group> getEmptyGroups(List<Item> source) {
Item start = source.get(0);
Calendar startTime = Calendar.getInstance();
startTime.setTime(formatter.parse(start.getTime()));
Item end = source.get(source.size() - 1);
Calendar endTime = Calendar.getInstance();
endTime.setTime(formatter.parse(end.getTime()));
long groupTotal = ChronoUnit.MINUTES.between(startTime.toInstant(), endTime.toInstant()) + 1;
List<Group> groups = new ArrayList<>();
IntStream.iterate(0, i -> i < groupTotal, i -> i + 1)
.forEachOrdered(i -> {
Group group = new Group();
group.setId(i + 1);
groups.add(group);
});
return groups;
}
}
Ответы (1 шт):
Прежде всего, у вас что-то не так с ожидаемыми значениями.
У объекта 2.2 метка времени "19/09/2020 1:04:00 AM", а должно быть "19/09/2020 1:04:30 AM", не так ли? Аналогично для объекта 3.2. Кстати, у последней группы номер должен быть 3, а не 2, правильно?
Непонятно среднее значение для объекта 2.2. У вас 1.5, но должно быть (1.9+2.1)/2 -> 2.0.
Теперь о решении.
Если решать по-простому, в лоб, без оптимизации по памяти, то я бы делал вот как.
Разбить период на непересекающиеся окна. Например, в вашем случае период 60 секунд разбит на два окна. Далее в бесконечном цикле - если объект принадлежит текущему интервалу, то добавить его в соответствующее окно, в противном случае закрыть интервал и начать следующий, до тех пор пока объект не окажется внутри интервала.
Закрыть окно означает сделать Item с меткой времени, соответствующей началу окна, и значением равным среднему значению за время окна.
Код для окна.
package org.example.groups;
import java.text.ParseException;
public class Window {
public static final long PERIOD = 60000;
public final int windowId;
public long start;
public long end;
public int count = 0;
public double sum = 0.0;
public double lastValue = 0.0;
public Window(long start, long duration, int windowId) {
this.windowId = windowId;
this.start = start;
this.end = start + duration;
}
public double add(double v) throws ParseException {
count +=1;
sum += v;
return value();
}
public double value() {
if (count == 0) {
return lastValue;
}
return sum / count;
}
public Item shift() {
return shift(1);
}
public Item shift(int n) {
Item result = toItem();
lastValue = value();
sum = 0.0;
count = 0;
start += n*PERIOD;
end += n*PERIOD;
return result;
}
public Item toItem() {
return new Item(
windowId,
Util.toDateString(start),
value()
);
}
public boolean isIn(long t) throws ParseException {
return (start <= t && t < end);
}
}
Код для подсчета средних значений:
package org.example.groups;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
public class Average {
public List<Item> items;
private Window[] windows;
private double lastValue = 0.0;
private final long duration;
public Average(long start, int numOfWindows) {
items = new LinkedList<Item>();
long intervalStart = Util.periodStart(start);
long intervalEnd = intervalStart + Window.PERIOD;
duration = Window.PERIOD / numOfWindows;
windows = new Window[numOfWindows];
for (int i = 0; i < numOfWindows; i++) {
windows[i] = new Window(intervalStart + duration*i, duration, i+1);
}
windows[numOfWindows-1].end = intervalEnd;
}
private long intervalStart() {
return windows[0].start;
}
private long intervalEnd() {
return intervalStart() + Window.PERIOD;
}
private boolean isIn(long t) {
return (intervalStart() <= t && t < intervalEnd());
}
private void finishPeriod() {
for (Window w : windows) {
w.lastValue = lastValue;
Item it = w.shift();
lastValue = it.getValue();
items.add(it);
}
}
public void add(Item it) throws ParseException {
long t = Util.timeInMillis(it);
while (!isIn(t)) {
finishPeriod();
}
int wIdx = (int) ((t - intervalStart()) / duration);
assert(wIdx < windows.length);
windows[wIdx].add(it.getValue());
}
public List<Item> finish() {
finishPeriod();
return items;
}
public List<Group> groupify() {
LinkedList<Group> result = new LinkedList<Group>();
int itemsPerGroup = windows.length;
int groupId = 1;
for (int i = 0; i < items.size(); i+=itemsPerGroup) {
Group g = new Group(groupId++, items.subList(i, i+itemsPerGroup));
result.add(g);
}
return result;
}
}
Класс Average параметризуется началом интервала и числом окошек внутри периода. Длительность периода задана константой Window.PERIOD в миллисекундах. Метод public void add(Item it) сдвигает интервал при необходимости и затем добавляет объект в соответствующее окно. При сдвиге интервала средние значения добавляются в список items.
Для простоты я пишу объекты в список сплошняком и только при необходимости собираю в группы, но можно было бы собирать группы внутри finishPeriod.
В чём это решение неоптимально.
Можно было бы обойтись одним окном, которое скользит внутри интервала.
В том случае, когда в данных большие просветы между отметками времени, можно было бы не сохранять "пустые"
Item, а только для тех интервалов, в которых были данные.
Полный код: https://github.com/pakuula/StackOverflow/tree/main/java/1450111
Пример запуска для двух интервалов в периоде:
package org.example.groups;
import java.text.ParseException;
import java.util.List;
public class Main {
public static void main(String[] args) throws ParseException {
List<Item> items = List.of(
new Item(1, "19/09/2020 1:03:00 AM", 1.0),
new Item(2, "19/09/2020 1:03:03 AM", 1.3),
new Item(3, "19/09/2020 1:03:15 AM", 1.1),
new Item(4, "19/09/2020 1:03:47 AM", 1.2),
new Item(5, "19/09/2020 1:03:57 AM", 1.6),
new Item(6, "19/09/2020 1:04:04 AM", 1.8),
new Item(7, "19/09/2020 1:04:43 AM", 1.9),
new Item(8, "19/09/2020 1:04:44 AM", 2.1),
new Item(9, "19/09/2020 1:05:30 AM", 1.8),
new Item(10, "19/09/2020 1:05:46 AM", 2.3)
);
long start = Util.timeInMillis(items.get(0));
start = Util.periodStart(start);
Average av = new Average(start, 2);
for (Item it : items) {
av.add(it);
}
av.finish()
for (Group g : av.groupify()) {
System.out.println(g.toString());
}
}
}
Вывод:
Group [id=1, items=[Item [id=1, time=Sat Sep 19 01:03:00 KST 2020, value=1.13], Item [id=2, time=Sat Sep 19 01:03:30 KST 2020, value=1.40]]]
Group [id=2, items=[Item [id=1, time=Sat Sep 19 01:04:00 KST 2020, value=1.80], Item [id=2, time=Sat Sep 19 01:04:30 KST 2020, value=2.00]]]
Group [id=3, items=[Item [id=1, time=Sat Sep 19 01:05:00 KST 2020, value=2.00], Item [id=2, time=Sat Sep 19 01:05:30 KST 2020, value=2.05]]]