VppBaselineHelper.java 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. package com.usky.vpp.util;
  2. import com.usky.vpp.constant.VppTsdbConstants;
  3. import com.usky.vpp.service.vo.BaselinePointVO;
  4. import com.usky.vpp.service.vo.DeclaredCapacityPointVO;
  5. import java.math.BigDecimal;
  6. import java.math.RoundingMode;
  7. import java.time.LocalDate;
  8. import java.time.LocalDateTime;
  9. import java.time.LocalTime;
  10. import java.time.format.DateTimeFormatter;
  11. import java.util.ArrayList;
  12. import java.util.Collections;
  13. import java.util.HashSet;
  14. import java.util.LinkedHashMap;
  15. import java.util.List;
  16. import java.util.Map;
  17. import java.util.Set;
  18. import java.util.TreeMap;
  19. import java.util.function.BiFunction;
  20. /**
  21. * 基线管理计算辅助(典型日筛选、原始基线、修正系数、曲线构建)
  22. */
  23. public final class VppBaselineHelper {
  24. public static final int WEEKDAY_REFERENCE_COUNT = 5;
  25. public static final int NON_WEEKDAY_REFERENCE_COUNT = 3;
  26. public static final int CORRECTION_HOURS = 2;
  27. public static final int INTERVAL_MINUTES = 5;
  28. public static final BigDecimal K_MIN = new BigDecimal("0.7");
  29. public static final BigDecimal K_MAX = new BigDecimal("1.2");
  30. public static final BigDecimal K_DEFAULT = BigDecimal.ONE;
  31. /** 总有功功率指标(文档 p + 存量字段兼容) */
  32. public static final List<String> POWER_METRICS;
  33. private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
  34. private static final DateTimeFormatter TIME_WITH_SEC_FMT = DateTimeFormatter.ofPattern("HH:mm:ss");
  35. private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
  36. static {
  37. List<String> metrics = new ArrayList<>();
  38. metrics.add("p");
  39. metrics.addAll(VppTsdbConstants.ACTIVE_POWER_METRICS);
  40. POWER_METRICS = Collections.unmodifiableList(metrics);
  41. }
  42. private VppBaselineHelper() {
  43. }
  44. /**
  45. * 响应日是否为工作日(Hutool 法定节假日 + 调休规则)
  46. */
  47. public static boolean isWorkdayResponse(LocalDate date) {
  48. return VppWorkdayHelper.isWorkday(date);
  49. }
  50. /**
  51. * @deprecated 使用 {@link #isWorkdayResponse(LocalDate)}
  52. */
  53. @Deprecated
  54. public static boolean isWeekday(LocalDate date) {
  55. return isWorkdayResponse(date);
  56. }
  57. /**
  58. * 选取典型历史日:工作日取前 5 个工作日,非工作日取前 3 个非工作日;
  59. * 仅剔除响应当日与历史响应日(不含节假日前后日期)。
  60. */
  61. public static List<LocalDate> selectReferenceDates(LocalDate responseDate,
  62. boolean workday,
  63. Set<LocalDate> excludeDates,
  64. int requiredCount) {
  65. Set<LocalDate> excludes = new HashSet<>(excludeDates);
  66. excludes.add(responseDate);
  67. List<LocalDate> result = new ArrayList<>();
  68. LocalDate cursor = responseDate.minusDays(1);
  69. int guard = 0;
  70. while (result.size() < requiredCount && guard < 400) {
  71. if (excludes.contains(cursor)) {
  72. cursor = cursor.minusDays(1);
  73. guard++;
  74. continue;
  75. }
  76. if (VppWorkdayHelper.isWorkday(cursor) == workday) {
  77. result.add(cursor);
  78. }
  79. cursor = cursor.minusDays(1);
  80. guard++;
  81. }
  82. return result;
  83. }
  84. /**
  85. * 多历史日同时段各时间点负荷求平均(先按日汇总设备负荷,再跨日取均值)。
  86. * <p>通过 historyLoader 按日调用 TSDB {@code queryHistoryDeviceData} 获取原始时序后在本模块聚合。</p>
  87. */
  88. public static Map<String, BigDecimal> aggregateBaselineAvgByTimePoint(
  89. List<LocalDate> referenceDates,
  90. LocalTime periodStart,
  91. LocalTime periodEnd,
  92. int intervalMinutes,
  93. BigDecimal correctionFactor,
  94. BiFunction<LocalDateTime, LocalDateTime, Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>>> historyLoader) {
  95. if (referenceDates == null || referenceDates.isEmpty() || historyLoader == null) {
  96. return Collections.emptyMap();
  97. }
  98. LocalTime dayStart = periodStart != null ? periodStart : LocalTime.MIN;
  99. LocalTime dayEnd = periodEnd != null ? periodEnd : LocalTime.of(23, 59, 59);
  100. int interval = intervalMinutes > 0 ? intervalMinutes : INTERVAL_MINUTES;
  101. Map<String, List<BigDecimal>> samplesByTime = new TreeMap<>();
  102. for (LocalDate date : referenceDates) {
  103. LocalDateTime start = LocalDateTime.of(date, dayStart);
  104. LocalDateTime end = LocalDateTime.of(date, dayEnd);
  105. if (end.isBefore(start)) {
  106. continue;
  107. }
  108. Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> dayData = historyLoader.apply(start, end);
  109. Map<String, BigDecimal> dayBuckets = buildActualLoadByTimePoint(dayData, interval);
  110. if (dayBuckets.isEmpty()) {
  111. continue;
  112. }
  113. for (Map.Entry<String, BigDecimal> entry : dayBuckets.entrySet()) {
  114. samplesByTime.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).add(entry.getValue());
  115. }
  116. }
  117. Map<String, BigDecimal> result = new LinkedHashMap<>();
  118. for (Map.Entry<String, List<BigDecimal>> entry : samplesByTime.entrySet()) {
  119. BigDecimal avg = average(entry.getValue());
  120. if (correctionFactor != null) {
  121. avg = avg.multiply(correctionFactor);
  122. }
  123. result.put(entry.getKey(), scale(avg));
  124. }
  125. return result;
  126. }
  127. /**
  128. * 将设备历史时序聚合为 HH:mm 时间点总负荷。
  129. */
  130. public static Map<String, BigDecimal> buildActualLoadByTimePoint(
  131. Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceData,
  132. int intervalMinutes) {
  133. if (deviceData == null || deviceData.isEmpty()) {
  134. return Collections.emptyMap();
  135. }
  136. Map<String, List<BigDecimal>> samples = new TreeMap<>();
  137. for (Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap : deviceData.values()) {
  138. TreeMap<LocalDateTime, BigDecimal> series = pickSeries(metricMap);
  139. if (series == null || series.isEmpty()) {
  140. continue;
  141. }
  142. Map<String, List<BigDecimal>> deviceBuckets = bucketSeries(series, intervalMinutes);
  143. for (Map.Entry<String, List<BigDecimal>> entry : deviceBuckets.entrySet()) {
  144. BigDecimal deviceAvg = average(entry.getValue());
  145. samples.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()).add(deviceAvg);
  146. }
  147. }
  148. Map<String, BigDecimal> result = new LinkedHashMap<>();
  149. for (Map.Entry<String, List<BigDecimal>> entry : samples.entrySet()) {
  150. result.put(entry.getKey(), scale(sum(entry.getValue())));
  151. }
  152. return result;
  153. }
  154. /**
  155. * 计算日内修正系数 K = 响应开始前 2 小时今日实测 / 同时段原始基线,并限制在 [0.7, 1.2]。
  156. */
  157. public static BigDecimal calculateCorrectionFactor(LocalDateTime responseStartTime,
  158. Map<String, BigDecimal> todayActualByTime,
  159. Map<String, BigDecimal> originalBaselineByTime) {
  160. LocalDateTime windowStart = responseStartTime.minusHours(CORRECTION_HOURS);
  161. BigDecimal todayTotal = sumValuesInWindow(todayActualByTime, windowStart, responseStartTime);
  162. BigDecimal baselineTotal = sumValuesInWindow(originalBaselineByTime, windowStart, responseStartTime);
  163. if (baselineTotal == null || baselineTotal.compareTo(BigDecimal.ZERO) <= 0) {
  164. return K_DEFAULT;
  165. }
  166. if (todayTotal == null || todayTotal.compareTo(BigDecimal.ZERO) <= 0) {
  167. return K_DEFAULT;
  168. }
  169. BigDecimal k = todayTotal.divide(baselineTotal, 6, RoundingMode.HALF_UP);
  170. if (k.compareTo(K_MIN) < 0) {
  171. return K_MIN;
  172. }
  173. if (k.compareTo(K_MAX) > 0) {
  174. return K_MAX;
  175. }
  176. return k.setScale(4, RoundingMode.HALF_UP);
  177. }
  178. public static List<BaselinePointVO> buildBaselinePoints(LocalDate responseDate,
  179. LocalDateTime responseStartTime,
  180. Map<String, BigDecimal> predictedBaselineByTime,
  181. Map<String, BigDecimal> todayActualByTime,
  182. Map<String, BigDecimal> yesterdayActualByTime) {
  183. return buildBaselinePoints(responseDate, responseStartTime, responseStartTime,
  184. predictedBaselineByTime, todayActualByTime, yesterdayActualByTime);
  185. }
  186. /**
  187. * @param todayActualUntil 当日实测曲线填充截止时间(含),用于响应后窗口展示
  188. */
  189. public static List<BaselinePointVO> buildBaselinePoints(LocalDate responseDate,
  190. LocalDateTime responseStartTime,
  191. LocalDateTime todayActualUntil,
  192. Map<String, BigDecimal> predictedBaselineByTime,
  193. Map<String, BigDecimal> todayActualByTime,
  194. Map<String, BigDecimal> yesterdayActualByTime) {
  195. List<BaselinePointVO> points = new ArrayList<>();
  196. LocalDateTime actualUntil = todayActualUntil != null ? todayActualUntil : responseStartTime;
  197. for (Map.Entry<String, BigDecimal> entry : predictedBaselineByTime.entrySet()) {
  198. String timeKey = entry.getKey();
  199. LocalTime time = parseTimeKey(timeKey);
  200. LocalDateTime pointTime = LocalDateTime.of(responseDate, time);
  201. BaselinePointVO point = new BaselinePointVO();
  202. point.setTime(timeKey);
  203. point.setPredictedBaselineKw(scale(entry.getValue()));
  204. if (!pointTime.isAfter(actualUntil)) {
  205. point.setTodayActualKw(scale(todayActualByTime.get(timeKey)));
  206. }
  207. point.setYesterdayActualKw(scale(yesterdayActualByTime.get(timeKey)));
  208. points.add(point);
  209. }
  210. return points;
  211. }
  212. public static boolean hasTsdbPowerData(Map<String, Map<String, TreeMap<LocalDateTime, BigDecimal>>> deviceMetricData) {
  213. return VppCapabilityEvalHelper.hasTsdbPowerData(deviceMetricData, POWER_METRICS);
  214. }
  215. /*
  216. public static List<BaselinePointVO> buildMockBaselinePoints(Long siteId,
  217. LocalDate responseDate,
  218. LocalDateTime responseStartTime) {
  219. List<BaselinePointVO> points = new ArrayList<>();
  220. long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
  221. for (int minute = 0; minute < 1440; minute += INTERVAL_MINUTES) {
  222. LocalTime time = LocalTime.of(minute / 60, minute % 60);
  223. String timeKey = time.format(TIME_FMT);
  224. double hourFactor = mockHourFactor(time);
  225. double noise = 0.9 + pseudoRandom(seed, minute, 0) * 0.2;
  226. BigDecimal yesterday = scale(BigDecimal.valueOf(900 * hourFactor * noise));
  227. BigDecimal original = yesterday.multiply(BigDecimal.valueOf(0.98 + pseudoRandom(seed, minute, 1) * 0.04))
  228. .setScale(2, RoundingMode.HALF_UP);
  229. BigDecimal predicted = original.multiply(BigDecimal.valueOf(1.03)).setScale(2, RoundingMode.HALF_UP);
  230. BaselinePointVO point = new BaselinePointVO();
  231. point.setTime(timeKey);
  232. point.setPredictedBaselineKw(predicted);
  233. point.setYesterdayActualKw(yesterday);
  234. if (!responseDate.atTime(time).isAfter(responseStartTime)) {
  235. point.setTodayActualKw(scale(original.multiply(BigDecimal.valueOf(1.01 + pseudoRandom(seed, minute, 2) * 0.05))));
  236. }
  237. points.add(point);
  238. }
  239. return points;
  240. }
  241. public static BigDecimal mockCorrectionFactor(Long siteId, LocalDate responseDate) {
  242. long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
  243. double raw = 0.85 + pseudoRandom(seed, 99, 3) * 0.25;
  244. BigDecimal k = BigDecimal.valueOf(raw).setScale(4, RoundingMode.HALF_UP);
  245. if (k.compareTo(K_MIN) < 0) {
  246. return K_MIN;
  247. }
  248. if (k.compareTo(K_MAX) > 0) {
  249. return K_MAX;
  250. }
  251. return k;
  252. }
  253. */
  254. public static String formatDate(LocalDate date) {
  255. return date.format(DATE_FMT);
  256. }
  257. private static final BigDecimal DECLARED_CAPACITY_DIVISOR = new BigDecimal("2");
  258. private static final BigDecimal MINUTES_PER_HOUR = new BigDecimal("60");
  259. /**
  260. * 响应执行时段小时数(执行开始至结束)。
  261. */
  262. public static BigDecimal responseWindowHours(LocalDateTime windowStart, LocalDateTime windowEnd) {
  263. if (windowStart == null || windowEnd == null || windowEnd.isBefore(windowStart)) {
  264. return BigDecimal.ZERO;
  265. }
  266. long minutes = java.time.Duration.between(windowStart, windowEnd).toMinutes();
  267. return BigDecimal.valueOf(minutes).divide(MINUTES_PER_HOUR, 4, RoundingMode.HALF_UP);
  268. }
  269. /**
  270. * 响应申报容量 = 响应时段内历史负荷均值 × 修正系数 K × 执行时段小时数 / 2。
  271. */
  272. public static BigDecimal calculateDeclaredCapacityKw(Map<String, BigDecimal> historicalAvgByTime,
  273. LocalDateTime windowStart,
  274. LocalDateTime windowEnd,
  275. BigDecimal correctionFactorK) {
  276. BigDecimal historicalAvg = averageValuesInWindow(historicalAvgByTime, windowStart, windowEnd);
  277. if (historicalAvg == null || historicalAvg.compareTo(BigDecimal.ZERO) <= 0) {
  278. return BigDecimal.ZERO;
  279. }
  280. return applyDeclaredCapacityFormula(historicalAvg, correctionFactorK, windowStart, windowEnd);
  281. }
  282. /**
  283. * 预估响应容量 = 响应时段内今日负荷均值。
  284. */
  285. public static BigDecimal calculateEstimatedResponseCapacityKw(Map<String, BigDecimal> historicalAvgByTime,
  286. LocalDateTime windowStart,
  287. LocalDateTime windowEnd) {
  288. BigDecimal historicalAvg = averageValuesInWindow(historicalAvgByTime, windowStart, windowEnd);
  289. if (historicalAvg == null || historicalAvg.compareTo(BigDecimal.ZERO) <= 0) {
  290. return BigDecimal.ZERO;
  291. }
  292. return historicalAvg;
  293. }
  294. private static BigDecimal applyDeclaredCapacityFormula(BigDecimal historicalAvg,
  295. BigDecimal correctionFactorK,
  296. LocalDateTime windowStart,
  297. LocalDateTime windowEnd) {
  298. BigDecimal k = correctionFactorK != null ? correctionFactorK : K_DEFAULT;
  299. BigDecimal windowHours = responseWindowHours(windowStart, windowEnd);
  300. return scale(historicalAvg.multiply(k).multiply(windowHours)
  301. .divide(DECLARED_CAPACITY_DIVISOR, 6, RoundingMode.HALF_UP));
  302. }
  303. public static List<DeclaredCapacityPointVO> buildDeclaredCapacityPoints(Map<String, BigDecimal> historicalAvgByTime,
  304. LocalDateTime windowStart,
  305. LocalDateTime windowEnd,
  306. BigDecimal correctionFactorK) {
  307. List<DeclaredCapacityPointVO> points = new ArrayList<>();
  308. if (historicalAvgByTime == null || historicalAvgByTime.isEmpty()) {
  309. return points;
  310. }
  311. LocalDate date = windowStart.toLocalDate();
  312. for (Map.Entry<String, BigDecimal> entry : historicalAvgByTime.entrySet()) {
  313. LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
  314. if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
  315. continue;
  316. }
  317. BigDecimal historicalAvg = entry.getValue() != null ? entry.getValue() : BigDecimal.ZERO;
  318. DeclaredCapacityPointVO point = new DeclaredCapacityPointVO();
  319. point.setTime(entry.getKey());
  320. point.setHistoricalAvgKw(scale(historicalAvg));
  321. point.setDeclaredCapacityKw(applyDeclaredCapacityFormula(
  322. historicalAvg, correctionFactorK, windowStart, windowEnd));
  323. points.add(point);
  324. }
  325. return points;
  326. }
  327. /*
  328. public static BigDecimal mockDeclaredCapacityKw(Long siteId,
  329. LocalDate responseDate,
  330. LocalDateTime windowStart,
  331. LocalDateTime windowEnd,
  332. BigDecimal correctionFactorK) {
  333. return calculateDeclaredCapacityKw(
  334. buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
  335. windowStart,
  336. windowEnd,
  337. correctionFactorK);
  338. }
  339. public static List<DeclaredCapacityPointVO> buildMockDeclaredCapacityPoints(Long siteId,
  340. LocalDate responseDate,
  341. LocalDateTime windowStart,
  342. LocalDateTime windowEnd,
  343. BigDecimal correctionFactorK) {
  344. return buildDeclaredCapacityPoints(
  345. buildMockHistoricalWindow(siteId, responseDate, windowStart, windowEnd),
  346. windowStart,
  347. windowEnd,
  348. correctionFactorK);
  349. }
  350. private static Map<String, BigDecimal> buildMockHistoricalWindow(Long siteId,
  351. LocalDate responseDate,
  352. LocalDateTime windowStart,
  353. LocalDateTime windowEnd) {
  354. Map<String, BigDecimal> mockHistorical = new TreeMap<>();
  355. long seed = (siteId != null ? siteId : 0L) + responseDate.toEpochDay();
  356. LocalDateTime cursor = windowStart;
  357. while (!cursor.isAfter(windowEnd)) {
  358. double hourFactor = mockHourFactor(cursor.toLocalTime());
  359. double noise = 0.9 + pseudoRandom(seed, cursor.getHour() * 60 + cursor.getMinute(), 4) * 0.2;
  360. mockHistorical.put(cursor.toLocalTime().format(TIME_FMT),
  361. BigDecimal.valueOf(900 * hourFactor * noise).setScale(2, RoundingMode.HALF_UP));
  362. cursor = cursor.plusMinutes(INTERVAL_MINUTES);
  363. }
  364. return mockHistorical;
  365. }
  366. */
  367. private static BigDecimal averageValuesInWindow(Map<String, BigDecimal> valuesByTime,
  368. LocalDateTime windowStart,
  369. LocalDateTime windowEnd) {
  370. if (valuesByTime == null || valuesByTime.isEmpty()) {
  371. return null;
  372. }
  373. List<BigDecimal> values = new ArrayList<>();
  374. LocalDate date = windowStart.toLocalDate();
  375. for (Map.Entry<String, BigDecimal> entry : valuesByTime.entrySet()) {
  376. LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
  377. if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
  378. continue;
  379. }
  380. if (entry.getValue() != null) {
  381. values.add(entry.getValue());
  382. }
  383. }
  384. return values.isEmpty() ? null : average(values);
  385. }
  386. private static Map<String, List<BigDecimal>> bucketSeries(TreeMap<LocalDateTime, BigDecimal> series,
  387. int intervalMinutes) {
  388. Map<String, List<BigDecimal>> buckets = new TreeMap<>();
  389. for (Map.Entry<LocalDateTime, BigDecimal> entry : series.entrySet()) {
  390. if (entry.getValue() == null) {
  391. continue;
  392. }
  393. String bucket = toBucketKey(entry.getKey(), intervalMinutes);
  394. buckets.computeIfAbsent(bucket, key -> new ArrayList<>()).add(entry.getValue());
  395. }
  396. return buckets;
  397. }
  398. private static String toBucketKey(LocalDateTime timestamp, int intervalMinutes) {
  399. int totalMinutes = timestamp.getHour() * 60 + timestamp.getMinute();
  400. int bucketMinutes = (totalMinutes / intervalMinutes) * intervalMinutes;
  401. return LocalTime.of(bucketMinutes / 60, bucketMinutes % 60).format(TIME_FMT);
  402. }
  403. private static LocalTime parseTimeKey(String timeKey) {
  404. if (timeKey.length() == 5) {
  405. return LocalTime.parse(timeKey + ":00", TIME_WITH_SEC_FMT);
  406. }
  407. return LocalTime.parse(timeKey, TIME_WITH_SEC_FMT);
  408. }
  409. private static BigDecimal sumValuesInWindow(Map<String, BigDecimal> valuesByTime,
  410. LocalDateTime windowStart,
  411. LocalDateTime windowEnd) {
  412. if (valuesByTime == null || valuesByTime.isEmpty()) {
  413. return null;
  414. }
  415. BigDecimal total = BigDecimal.ZERO;
  416. int count = 0;
  417. LocalDate date = windowStart.toLocalDate();
  418. for (Map.Entry<String, BigDecimal> entry : valuesByTime.entrySet()) {
  419. LocalDateTime pointTime = LocalDateTime.of(date, parseTimeKey(entry.getKey()));
  420. if (pointTime.isBefore(windowStart) || pointTime.isAfter(windowEnd)) {
  421. continue;
  422. }
  423. if (entry.getValue() != null) {
  424. total = total.add(entry.getValue());
  425. count++;
  426. }
  427. }
  428. return count == 0 ? null : total;
  429. }
  430. private static TreeMap<LocalDateTime, BigDecimal> pickSeries(Map<String, TreeMap<LocalDateTime, BigDecimal>> metricMap) {
  431. if (metricMap == null || metricMap.isEmpty()) {
  432. return null;
  433. }
  434. for (String metric : POWER_METRICS) {
  435. TreeMap<LocalDateTime, BigDecimal> series = metricMap.get(metric);
  436. if (series != null && !series.isEmpty()) {
  437. return series;
  438. }
  439. }
  440. for (TreeMap<LocalDateTime, BigDecimal> series : metricMap.values()) {
  441. if (series != null && !series.isEmpty()) {
  442. return series;
  443. }
  444. }
  445. return null;
  446. }
  447. private static BigDecimal sum(List<BigDecimal> values) {
  448. if (values == null || values.isEmpty()) {
  449. return BigDecimal.ZERO;
  450. }
  451. BigDecimal total = BigDecimal.ZERO;
  452. for (BigDecimal value : values) {
  453. if (value != null) {
  454. total = total.add(value);
  455. }
  456. }
  457. return total;
  458. }
  459. private static BigDecimal average(List<BigDecimal> values) {
  460. if (values == null || values.isEmpty()) {
  461. return BigDecimal.ZERO;
  462. }
  463. return sum(values).divide(BigDecimal.valueOf(values.size()), 4, RoundingMode.HALF_UP);
  464. }
  465. private static BigDecimal scale(BigDecimal value) {
  466. if (value == null) {
  467. return null;
  468. }
  469. return value.setScale(2, RoundingMode.HALF_UP);
  470. }
  471. /*
  472. private static double mockHourFactor(LocalTime time) {
  473. int hour = time.getHour();
  474. int minute = time.getMinute();
  475. double h = hour + minute / 60.0;
  476. if (h < 6) {
  477. return 0.45 + h / 6 * 0.15;
  478. }
  479. if (h < 9) {
  480. return 0.6 + (h - 6) / 3 * 0.35;
  481. }
  482. if (h < 12) {
  483. return 0.95 + (h - 9) / 3 * 0.05;
  484. }
  485. if (h < 14) {
  486. return 0.85;
  487. }
  488. if (h < 17) {
  489. return 0.9 + (h - 14) / 3 * 0.1;
  490. }
  491. if (h < 21) {
  492. return 1.0 - (h - 17) / 4 * 0.35;
  493. }
  494. return 0.5 - (h - 21) / 3 * 0.15;
  495. }
  496. private static double pseudoRandom(long seed, int minuteOfDay, int seriesSalt) {
  497. long mixed = seed * 31L + minuteOfDay * 17L + seriesSalt * 13L;
  498. return (mixed % 1000) / 1000.0;
  499. }
  500. */
  501. }