package com.ruoyi.http.util;
|
|
import com.ruoyi.http.vo.StatisticEleComparisonVo;
|
import com.ruoyi.http.vo.StatisticEleRecordVo;
|
import com.ruoyi.http.vo.StatisticEleSplitItemVo;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.DayOfWeek;
|
import java.time.LocalDate;
|
import java.time.YearMonth;
|
import java.time.format.DateTimeFormatter;
|
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.WeekFields;
|
import java.util.ArrayList;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.function.Function;
|
|
/**
|
* 能耗综合分析计算
|
*/
|
public final class StatisticEleAnalyticsUtil {
|
|
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
private static final int SUMMARY_SCALE = 2;
|
/** 白班 08:00-19:59 */
|
private static final int DAY_SHIFT_START = 8;
|
private static final int DAY_SHIFT_END = 19;
|
|
private StatisticEleAnalyticsUtil() {
|
}
|
|
public static final Function<String, String> HOUR_TO_WEEK = tk -> {
|
if (tk == null || tk.length() < 8) {
|
return null;
|
}
|
LocalDate date = LocalDate.parse(tk.substring(0, 8), DAY_FMT);
|
WeekFields wf = WeekFields.of(Locale.CHINA);
|
int week = date.get(wf.weekOfWeekBasedYear());
|
int year = date.get(wf.weekBasedYear());
|
return String.format("%04dW%02d", year, week);
|
};
|
|
public static Function<String, String> trendBucketFn(String granularity) {
|
return switch (granularity) {
|
case "hour" -> StatisticEleAggregateUtil.HOUR_TO_HOUR;
|
case "week" -> HOUR_TO_WEEK;
|
case "month" -> StatisticEleAggregateUtil.HOUR_TO_MONTH;
|
case "year" -> StatisticEleAggregateUtil.HOUR_TO_YEAR;
|
default -> StatisticEleAggregateUtil.HOUR_TO_DAY;
|
};
|
}
|
|
/**
|
* 周期累计来自汇总桶,极值/均值始终来自小时桶。
|
*/
|
public static StatisticEleAggregateUtil.StatisticEleSummaryMetrics calcCombinedMetrics(
|
List<StatisticEleRecordVo> hourRecords,
|
List<StatisticEleRecordVo> periodChartRecords) {
|
List<StatisticEleRecordVo> hourlyBuckets = StatisticEleAggregateUtil.aggregateHourToBuckets(
|
hourRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR);
|
StatisticEleAggregateUtil.StatisticEleSummaryMetrics hourMetrics =
|
StatisticEleAggregateUtil.calcMetrics(hourlyBuckets);
|
StatisticEleAggregateUtil.StatisticEleSummaryMetrics periodMetrics =
|
StatisticEleAggregateUtil.calcMetrics(periodChartRecords);
|
|
StatisticEleAggregateUtil.StatisticEleSummaryMetrics result =
|
new StatisticEleAggregateUtil.StatisticEleSummaryMetrics();
|
result.setRecordCount(periodMetrics.getRecordCount());
|
result.setTotalConsumption(hourMetrics.getTotalConsumption());
|
result.setAvgConsumption(hourMetrics.getAvgConsumption());
|
result.setMaxConsumption(hourMetrics.getMaxConsumption());
|
result.setMinConsumption(hourMetrics.getMinConsumption());
|
return result;
|
}
|
|
public static double calcLoadRate(Double avgHourly, Double maxHourly) {
|
if (avgHourly == null || maxHourly == null || maxHourly <= 0) {
|
return 0.0;
|
}
|
return roundSummary(BigDecimal.valueOf(avgHourly)
|
.divide(BigDecimal.valueOf(maxHourly), SUMMARY_SCALE + 2, RoundingMode.HALF_UP)
|
.multiply(BigDecimal.valueOf(100)));
|
}
|
|
public static List<StatisticEleSplitItemVo> calcPeriodSplits(List<StatisticEleRecordVo> hourRecords) {
|
double sharp = sumField(hourRecords, StatisticEleRecordVo::getSharpConsumption);
|
double peak = sumField(hourRecords, StatisticEleRecordVo::getPeakConsumption);
|
double flat = sumField(hourRecords, StatisticEleRecordVo::getFlatConsumption);
|
double valley = sumField(hourRecords, StatisticEleRecordVo::getValleyConsumption);
|
double total = sharp + peak + flat + valley;
|
if (total <= 0) {
|
total = sumField(hourRecords, StatisticEleRecordVo::getTotalConsumption);
|
}
|
List<StatisticEleSplitItemVo> items = new ArrayList<>();
|
addSplitItem(items, "尖", sharp, total);
|
addSplitItem(items, "峰", peak, total);
|
addSplitItem(items, "平", flat, total);
|
addSplitItem(items, "谷", valley, total);
|
return items.stream().filter(i -> i.getConsumption() != null && i.getConsumption() > 0).toList();
|
}
|
|
public static List<StatisticEleSplitItemVo> calcShiftSplits(List<StatisticEleRecordVo> hourRecords) {
|
Map<String, Double> map = new LinkedHashMap<>();
|
map.put("白班", 0.0);
|
map.put("夜班", 0.0);
|
for (StatisticEleRecordVo record : hourRecords) {
|
int hour = parseHour(record.getTimeKey());
|
if (hour < 0) {
|
continue;
|
}
|
double val = safe(record.getTotalConsumption());
|
if (hour >= DAY_SHIFT_START && hour <= DAY_SHIFT_END) {
|
map.merge("白班", val, Double::sum);
|
} else {
|
map.merge("夜班", val, Double::sum);
|
}
|
}
|
return toSplitItems(map);
|
}
|
|
public static List<StatisticEleSplitItemVo> calcDayTypeSplits(List<StatisticEleRecordVo> hourRecords) {
|
Map<String, Double> map = new LinkedHashMap<>();
|
map.put("工作日", 0.0);
|
map.put("休息日", 0.0);
|
for (StatisticEleRecordVo record : hourRecords) {
|
LocalDate date = parseDate(record.getTimeKey());
|
if (date == null) {
|
continue;
|
}
|
double val = safe(record.getTotalConsumption());
|
DayOfWeek dow = date.getDayOfWeek();
|
if (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY) {
|
map.merge("休息日", val, Double::sum);
|
} else {
|
map.merge("工作日", val, Double::sum);
|
}
|
}
|
return toSplitItems(map);
|
}
|
|
public static StatisticEleComparisonVo buildComparison(
|
String type, String label, Double currentTotal, Double compareTotal) {
|
StatisticEleComparisonVo vo = new StatisticEleComparisonVo();
|
vo.setType(type);
|
vo.setLabel(label);
|
vo.setCurrentTotal(round(currentTotal));
|
vo.setCompareTotal(round(compareTotal));
|
double delta = round(safe(currentTotal) - safe(compareTotal));
|
vo.setDelta(delta);
|
if (compareTotal != null && compareTotal > 0) {
|
vo.setChangeRate(roundSummary(BigDecimal.valueOf(delta)
|
.divide(BigDecimal.valueOf(compareTotal), SUMMARY_SCALE + 2, RoundingMode.HALF_UP)
|
.multiply(BigDecimal.valueOf(100))));
|
} else {
|
vo.setChangeRate(currentTotal != null && currentTotal > 0 ? 100.0 : 0.0);
|
}
|
return vo;
|
}
|
|
public record DateBounds(LocalDate start, LocalDate end) {}
|
|
public static DateBounds resolveBounds(String dimension, String startTime, String endTime) {
|
LocalDate start = resolveStartDate(dimension, startTime);
|
LocalDate end = resolveEndDate(dimension, endTime);
|
if (start.isAfter(end)) {
|
LocalDate tmp = start;
|
start = end;
|
end = tmp;
|
}
|
return new DateBounds(start, end);
|
}
|
|
public static DateBounds shiftChain(DateBounds bounds) {
|
long days = ChronoUnit.DAYS.between(bounds.start(), bounds.end()) + 1;
|
return new DateBounds(bounds.start().minusDays(days), bounds.start().minusDays(1));
|
}
|
|
public static DateBounds shiftYoy(DateBounds bounds) {
|
return new DateBounds(bounds.start().minusYears(1), bounds.end().minusYears(1));
|
}
|
|
public static String defaultTrendGranularity(String dimension, boolean singleDay) {
|
if (singleDay) {
|
return "hour";
|
}
|
return switch (dimension) {
|
case "week", "day" -> "day";
|
case "month", "quarter" -> "week";
|
case "year" -> "month";
|
default -> "day";
|
};
|
}
|
|
private static LocalDate resolveStartDate(String dimension, String startTime) {
|
return switch (dimension) {
|
case "year" -> LocalDate.of(Integer.parseInt(startTime), 1, 1);
|
case "month" -> YearMonth.parse(startTime, DateTimeFormatter.ofPattern("yyyyMM")).atDay(1);
|
default -> LocalDate.parse(normalizeDayKey(startTime), DAY_FMT);
|
};
|
}
|
|
private static LocalDate resolveEndDate(String dimension, String endTime) {
|
return switch (dimension) {
|
case "year" -> LocalDate.of(Integer.parseInt(endTime), 12, 31);
|
case "month" -> YearMonth.parse(endTime, DateTimeFormatter.ofPattern("yyyyMM")).atEndOfMonth();
|
default -> LocalDate.parse(normalizeDayKey(endTime), DAY_FMT);
|
};
|
}
|
|
private static String normalizeDayKey(String timeKey) {
|
if (timeKey == null) {
|
return LocalDate.now().format(DAY_FMT);
|
}
|
if (timeKey.length() == 4) {
|
return timeKey + "0101";
|
}
|
if (timeKey.length() == 6) {
|
return timeKey + "01";
|
}
|
return timeKey.length() >= 8 ? timeKey.substring(0, 8) : timeKey;
|
}
|
|
private static List<StatisticEleSplitItemVo> toSplitItems(Map<String, Double> map) {
|
double total = map.values().stream().mapToDouble(v -> v).sum();
|
List<StatisticEleSplitItemVo> items = new ArrayList<>();
|
map.forEach((name, val) -> addSplitItem(items, name, val, total));
|
return items;
|
}
|
|
private static void addSplitItem(List<StatisticEleSplitItemVo> items, String name, double val, double total) {
|
if (val <= 0) {
|
return;
|
}
|
StatisticEleSplitItemVo item = new StatisticEleSplitItemVo();
|
item.setName(name);
|
item.setConsumption(round(val));
|
item.setRatio(total > 0 ? roundSummary(BigDecimal.valueOf(val)
|
.divide(BigDecimal.valueOf(total), SUMMARY_SCALE + 2, RoundingMode.HALF_UP)
|
.multiply(BigDecimal.valueOf(100))) : 0.0);
|
items.add(item);
|
}
|
|
private static double sumField(List<StatisticEleRecordVo> records,
|
Function<StatisticEleRecordVo, Double> getter) {
|
return records.stream().map(getter).mapToDouble(StatisticEleAnalyticsUtil::safe).sum();
|
}
|
|
private static int parseHour(String timeKey) {
|
if (timeKey == null || timeKey.length() < 10) {
|
return -1;
|
}
|
try {
|
return Integer.parseInt(timeKey.substring(8, 10));
|
} catch (NumberFormatException e) {
|
return -1;
|
}
|
}
|
|
private static LocalDate parseDate(String timeKey) {
|
if (timeKey == null || timeKey.length() < 8) {
|
return null;
|
}
|
try {
|
return LocalDate.parse(timeKey.substring(0, 8), DAY_FMT);
|
} catch (Exception e) {
|
return null;
|
}
|
}
|
|
private static double safe(Double v) {
|
return v == null ? 0.0 : v;
|
}
|
|
private static double round(Double v) {
|
if (v == null) {
|
return 0.0;
|
}
|
return roundSummary(BigDecimal.valueOf(v));
|
}
|
|
private static double roundSummary(BigDecimal value) {
|
return value.setScale(SUMMARY_SCALE, RoundingMode.HALF_UP).doubleValue();
|
}
|
}
|