From aee8c10effc39d1203b7bd80c9c89b3c54bee5b9 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 08 九月 2026 10:14:14 +0800
Subject: [PATCH] feat: 汇总大表数据驱动 M1-网约车——新增 WycSplitCalc 拆分计算服务与 7 月回归测试报告

---
 docs/功能测试报告/功能测试报告_M1-网约车拆分WycSplitCalc_2026-09-08.md                                   |   33 ++++++
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java |  266 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 299 insertions(+), 0 deletions(-)

diff --git "a/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_M1-\347\275\221\347\272\246\350\275\246\346\213\206\345\210\206WycSplitCalc_2026-09-08.md" "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_M1-\347\275\221\347\272\246\350\275\246\346\213\206\345\210\206WycSplitCalc_2026-09-08.md"
new file mode 100644
index 0000000..5b22be6
--- /dev/null
+++ "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_M1-\347\275\221\347\272\246\350\275\246\346\213\206\345\210\206WycSplitCalc_2026-09-08.md"
@@ -0,0 +1,33 @@
+# 鍔熻兘娴嬭瘯鎶ュ憡锛歁1-缃戠害杞� WycSplitCalc 鍙栨暟/鎷嗗垎鏍稿涓� 7 鏈堝洖褰掞紙2026-09-08锛�
+
+## 涓�銆佸姛鑳借鏄�
+M1 绗笁鍧楋細鏂板缃戠害杞︽媶鍒嗚绠楁湇鍔� `WycSplitCalc`锛坄traffic-audit-server/.../reportexport/calc/WycSplitCalc.java`锛夛紝鎶� exportWycSplit 鐨勬媶鍒嗙畻娉曟娊鎴愬彲澶嶇敤鐭╅樀锛氳緭鍏�=銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬶紙pin B-E + 17 甯傚窞璁㈠崟 G-W锛�+ 宸℃父鍑虹鏈堟姤锛圖B锛夛紝杈撳嚭 17 甯傚窞+鍏ㄧ渷鐨� 瀹㈣繍閲�/鍩庡競鍐呭杩愰噺/鍛ㄨ浆閲�/鍩庡競鍐呭懆杞噺銆備緵姹囨�诲ぇ琛ㄧ綉绾﹁溅鍙拌处椤典笌鍩庡競瀹㈣繍鍚勯〉鍏辩敤锛堝彛寰� B5锛夈��
+
+## 浜屻�佹祴璇曠幆澧�
+- 搴擄細`traffic_audit`锛�2026-07 鐜扮姸锛夛紱
+- 杈撳叆鏂囦欢锛歚docs/鍩庡競瀹㈣繍/妯℃澘_缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx`锛堟湀搴﹁緭鍏� sheet锛�2026-07 琛屽湪 openpyxl 绗� 8 琛岋級锛�
+- 瀵硅处姣嶇増锛歚docs/鐢熸垚姹囨�诲ぇ琛�/_澶囦唤_2026骞�7鏈堥亾璺繍杈撻噺姹囨�昏〃_娓呯悊鍓峗20260904.xlsx` 缃戠害杞﹀彴璐﹂〉锛�
+- 宸ュ叿锛歰penpyxl + mysql CLI + Python 澶嶅埢绠楁硶姣斿锛涘悗绔� `mvn -q -DskipTests compile` 閫氳繃銆�
+
+## 涓夈�佺畻娉曪紙涓� exportWycSplit 涓�鑷达級
+- C[i]=璁㈠崟鍗犳瘮脳鍏ㄧ渷缃戠害杞﹀杩愰噺 pinK锛汧[i]=宸℃父鍑虹鍩庡競鍐呭杩愰噺/瀹㈣繍閲忥紙鍩庡競鍐呭崰姣斾唬鐞嗭級锛�
+- 鍩庡競鍐� H锛氭姹�=C0脳F0锛堜繚鎸佽嚜韬級锛涘叾浣� = (Ci脳Fi)/(危G鈭扖0F0) 脳 (pinCK鈭扖0F0)锛�
+- 鍛ㄨ浆閲� Gl[i]=Ci脳骞冲潎杩愯窛 / 危(Ci脳骞冲潎杩愯窛) 脳 pinZ锛涘煄甯傚唴鍛ㄨ浆閲� Hl 鍚屾硶锛堢敤鍩庡競鍐呭钩鍧囪繍璺濄�乸inCZ锛夛紱
+- 宸℃父鍑虹骞冲潎杩愯窛缂虹渷鏃舵寜 鍛ㄨ浆閲�/瀹㈣繍閲� 鍙� 2 浣嶅皬鏁板厹搴曘��
+
+## 鍥涖�佺敤渚嬭〃
+| # | 鐢ㄤ緥 | 杈撳叆 | 棰勬湡 | 瀹為檯 | 缁撹 |
+|---|---|---|---|---|---|
+| 1 | 2026-07 鎷嗗垎锛�17 甯傚窞脳4 鎸囨爣锛� | 璁㈠崟+pin+鍑虹 DB | 涓庢瘝鐗堢綉绾﹁溅鍙拌处 O 鍒椾竴鑷� | diff=0 | 閫氳繃 |
+| 2 | 鍏ㄧ渷琛� | pin 杈撳叆 | 瀹㈣繍閲� 4852.30 / 鍛ㄨ浆閲� 28348.02 / 鍩庡競鍐� 4430.84 / 鍩庡競鍐呭懆杞� 22705.61 | 涓庢瘝鐗堜竴鑷� | 閫氳繃 |
+| 3 | 鍔犳�昏嚜娲� | 璁$畻鏁扮粍 | 危C=pinK銆佄=pinCK銆佄l=pinZ銆佄l=pinCZ | 鍏ㄧ瓑 | 閫氳繃 |
+| 4 | 姝︽眽鍙e緞 | 璁$畻 | 鍩庡競鍐�=鎬婚噺锛堟棤鍩庨檯鍩庝埂锛夛紝H=1825.1975銆丠l=13036.3929 | 涓�鑷� | 閫氳繃 |
+| 5 | Java 缂栬瘧 | `mvn -q -DskipTests compile` | 鏃犻敊璇� | exit=0 | 閫氳繃 |
+
+## 浜斻�佸彂鐜扮殑闂涓庡缃�
+1. 杈撳叆鏂囦欢鐜扮姸鍚嶇О涓� `妯℃澘_缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx`锛岃�� ReportExportService.exportWycSplit 浠嶆寜绮剧‘鍚� `缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx` 瀹氫綅锛坮esolveAnyTemplate 涓嶅仛鍓嶇紑鍖归厤锛夆啋 璇ュ皬琛ㄧ敓鎴愯矾寰勫綋鍓嶅彲鑳芥姤鈥滄湭鎵惧埌妯℃澘鈥濄�俉ycSplitCalc.findInput 宸叉敮鎸佷袱绉嶅懡鍚嶏紝鍏堜繚璇佸ぇ琛ㄥ彇鏁板彲鐢紱灏忚〃鍏ュ彛鐨勬ā鏉垮畾浣嶅緟 M4 缁熶竴鎺ョ嚎鏃朵慨姝c��
+2. 绫诲悕涓庢枃浠跺悕涓嶄竴鑷存浘瀵艰嚧 javac 澶辫触骞剁壍杩炴棤鍏虫枃浠舵姤閿欙紙javac 閿欒鎭㈠绾ц仈锛夆啋 宸茬粺涓�涓� WycSplitCalc.java锛岀紪璇戦�氳繃銆�
+
+## 鍏�佺粨璁轰笌涓嬩竴姝�
+- 缁撹锛氱綉绾﹁溅鎷嗗垎鍙畬鏁村绠� 7 鏈堟瘝鐗堬紙0 宸紓锛夛紝`WycSplitCalc` 宸插叆搴撳苟閫氳繃缂栬瘧銆�
+- 涓嬩竴姝ワ細鍩庡競瀹㈣繍鍙拌处/鍩庡競姹囨��/鍩庡競鍒嗗競宸� 7 鏈堟暣閾惧洖褰掞紙鍏氦+鍑虹+缃戠害+杞ㄩ亾杞浮缁勫悎鍙e緞 C7锛夆啋 M1-MidCalc 鈫� M2 SummaryFiller銆�
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java
new file mode 100644
index 0000000..baf3d26
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java
@@ -0,0 +1,266 @@
+package com.trafficaudit.reportexport.calc;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.trafficaudit.common.util.RegionUtil;
+import com.trafficaudit.dataimport.entity.CityTaxiMonthly;
+import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 缃戠害杞︽媶鍒嗗彛寰勮绠楁湇鍔★紙姹囨�诲ぇ琛ㄦ暟鎹┍鍔ㄦ敼閫� M1-缃戠害杞︼紝2026-09-08锛夈��
+ * 绠楁硶涓� exportWycSplit / 銆婄綉绾﹁溅鎷嗗垎.xlsx銆嬫湀椤典竴鑷达紙B5锛氳鍗�+鍏ㄧ渷 pin+宸℃父鍑虹鎷嗗垎锛夛細
+ * 瀹㈣繍閲�=璁㈠崟鍗犳瘮脳鍏ㄧ渷鎬婚噺锛涘煄甯傚唴=宸℃父鍑虹鍩庡競鍐呭崰姣斾唬鐞嗭紙姝︽眽淇濇寔鑷韩銆佸叾浣欐寜鍓╀綑鍗犳瘮鍒嗘憡鍒板叏鐪佸煄甯傚唴 pin锛夛紱
+ * 鍛ㄨ浆閲�=瀹㈣繍閲徝楀贰娓稿嚭绉熷钩鍧囪繍璺濆崰姣斿垎鎽婂叏鐪佸懆杞噺 pin锛涘煄甯傚唴鍛ㄨ浆閲忓悓娉曪紙鐢ㄥ煄甯傚唴骞冲潎杩愯窛锛夈��
+ * 2026-07 涓� _澶囦唤鈥娓呯悊鍓峗20260904.xlsx 缃戠害杞﹀彴璐﹂〉 O 鍒楀洖褰� 0 宸紓锛�17 甯傚窞脳瀹㈣繍閲�/鍛ㄨ浆閲�/鍩庡競鍐呭杩愰噺/鍩庡競鍐呭懆杞噺锛夈��
+ */
+@Service
+public class WycSplitCalc {
+
+    /** 鍗曞競宸炴媶鍒嗙粨鏋滐紙鍚煄闄呭煄涔″彛寰勶紝渚涗笉鍚屽彴璐﹁浣跨敤锛� */
+    public static class WycMetrics {
+        private final double totalPax;       // 瀹㈣繍閲忔�婚噺锛堜竾浜猴級
+        private final double cityPax;        // 鍩庡競鍐呭杩愰噺锛堜竾浜猴級
+        private final double suburbanPax;    // 鍩庨檯鍩庝埂瀹㈣繍閲� = totalPax - cityPax
+        private final double totalTurnover;  // 鏃呭鍛ㄨ浆閲忔�婚噺锛堜竾浜哄叕閲岋級
+        private final double cityTurnover;   // 鍩庡競鍐呮梾瀹㈠懆杞噺锛堜竾浜哄叕閲岋級
+        private final double suburbanTurnover;
+
+        public WycMetrics(double totalPax, double cityPax, double totalTurnover, double cityTurnover) {
+            this.totalPax = totalPax;
+            this.cityPax = cityPax;
+            this.suburbanPax = totalPax - cityPax;
+            this.totalTurnover = totalTurnover;
+            this.cityTurnover = cityTurnover;
+            this.suburbanTurnover = totalTurnover - cityTurnover;
+        }
+
+        public double getTotalPax() { return totalPax; }
+        public double getCityPax() { return cityPax; }
+        public double getSuburbanPax() { return suburbanPax; }
+        public double getTotalTurnover() { return totalTurnover; }
+        public double getCityTurnover() { return cityTurnover; }
+        public double getSuburbanTurnover() { return suburbanTurnover; }
+    }
+
+    public static class WycResult {
+        private final String period;
+        private final Map<String, WycMetrics> byCity;   // key=RegionUtil 瑙勮寖甯傚窞鍚�
+        private final WycMetrics province;              // 鍏ㄧ渷 = pin 杈撳叆
+
+        public WycResult(String period, Map<String, WycMetrics> byCity, WycMetrics province) {
+            this.period = period;
+            this.byCity = byCity;
+            this.province = province;
+        }
+
+        public String getPeriod() { return period; }
+        public Map<String, WycMetrics> getByCity() { return byCity; }
+        public WycMetrics getProvince() { return province; }
+    }
+
+    @Resource
+    private CityTaxiMonthlyMapper cityTaxiMapper;
+    @Value("${city-passenger.template-dir:docs/鍩庡競瀹㈣繍}")
+    private String templateDir;
+
+    private static final String[] INPUT_CANDIDATES = {"缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx", "妯℃澘_缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx"};
+
+    /** 璁$畻 period锛堝 2026-07锛夌綉绾﹁溅鎷嗗垎缁撴灉 */
+    public WycResult calc(String period) throws Exception {
+        Map<String, Object> input = loadMonthlyInput(period);
+        if (input == null) {
+            throw new RuntimeException("銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬬己灏戞姤琛ㄦ湡 " + period + " 鐨勬暟鎹锛岃鍏堢淮鎶ゅ綋鏈堣鍗曚笌鍏ㄧ渷 pin");
+        }
+        double pinK = (Double) input.get("pinK");
+        double pinCK = (Double) input.get("pinCK");
+        double pinZ = (Double) input.get("pinZ");
+        double pinCZ = (Double) input.get("pinCZ");
+        double orderSum = (Double) input.get("orderSum");
+        double[] orderArr = (double[]) input.get("orders");
+        if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) {
+            throw new RuntimeException("缃戠害杞﹁緭鍏� " + period + " 琛屼笉瀹屾暣锛氬叏鐪� pin / 璁㈠崟缂哄け锛岃鏍稿銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆�");
+        }
+        List<String> cities = RegionUtil.cityList();
+        int n = cities.size();
+        if (orderArr == null || orderArr.length < n) {
+            throw new RuntimeException("缃戠害杞﹁鍗曞垪鏁颁笉瓒筹紙搴斾负 17 甯傚窞璁㈠崟鍒楋級");
+        }
+        Map<String, double[]> taxi = taxiMonthlyOf(period);
+        List<String> missingTaxi = new ArrayList<>();
+        double[] Dtax = new double[n];
+        double[] Etax = new double[n];
+        double[] avgD = new double[n];
+        double[] avgDc = new double[n];
+        for (int i = 0; i < n; i++) {
+            double[] a = taxi.get(cities.get(i));
+            if (a == null || a[0] <= 0) {
+                missingTaxi.add(cities.get(i));
+                continue;
+            }
+            Dtax[i] = a[0];
+            Etax[i] = a[2];
+            avgD[i] = a[4];
+            avgDc[i] = a[5];
+        }
+        if (!missingTaxi.isEmpty()) {
+            throw new RuntimeException("宸℃父鍑虹鏈堟姤缂哄皯浠ヤ笅甯傚窞鏁版嵁锛�" + String.join("銆�", missingTaxi));
+        }
+        double[] C = new double[n];
+        double[] H = new double[n];
+        double[] Gl = new double[n];
+        double[] Hl = new double[n];
+        double sumG = 0.0;
+        double sumDl = 0.0;
+        double[] cityRatio = new double[n];
+        for (int i = 0; i < n; i++) {
+            C[i] = orderArr[i] / orderSum * pinK;
+            cityRatio[i] = Etax[i] / Dtax[i];
+            sumG += C[i] * cityRatio[i];
+            sumDl += C[i] * avgD[i];
+        }
+        for (int i = 0; i < n; i++) {
+            if (i == 0) {
+                H[i] = C[i] * cityRatio[i];
+            } else {
+                double rest = sumG - C[0] * cityRatio[0];
+                H[i] = rest == 0.0 ? 0.0 : (C[i] * cityRatio[i]) / rest * (pinCK - C[0] * cityRatio[0]);
+            }
+        }
+        double[] el2 = new double[n];
+        double sumEl2 = 0.0;
+        for (int i = 0; i < n; i++) {
+            el2[i] = H[i] * avgDc[i];
+            sumEl2 += el2[i];
+            Gl[i] = sumDl == 0.0 ? 0.0 : C[i] * avgD[i] / sumDl * pinZ;
+        }
+        for (int i = 0; i < n; i++) {
+            if (i == 0) {
+                Hl[i] = Gl[i];
+            } else {
+                double rest = sumEl2 - el2[0];
+                Hl[i] = rest == 0.0 ? 0.0 : el2[i] / rest * (pinCZ - Gl[0]);
+            }
+        }
+        Map<String, WycMetrics> byCity = new LinkedHashMap<>();
+        for (int i = 0; i < n; i++) {
+            byCity.put(cities.get(i), new WycMetrics(C[i], H[i], Gl[i], Hl[i]));
+        }
+        WycMetrics province = new WycMetrics(pinK, pinCK, pinZ, pinCZ);
+        return new WycResult(period, byCity, province);
+    }
+
+    /** 宸℃父鍑虹鏈堟姤锛圖B锛夋寜鏈堝姞杞斤細甯傚窞 -> [瀹㈣繍閲�,鍛ㄨ浆閲�,鍩庡競鍐呭杩愰噺,鍩庡競鍐呭懆杞噺,骞冲潎杩愯窛,鍩庡競鍐呭钩鍧囪繍璺漖 */
+    private Map<String, double[]> taxiMonthlyOf(String period) {
+        Map<String, double[]> map = new LinkedHashMap<>();
+        List<CityTaxiMonthly> rows = cityTaxiMapper.selectList(
+            new LambdaQueryWrapper<CityTaxiMonthly>().eq(CityTaxiMonthly::getReportPeriod, period));
+        for (CityTaxiMonthly r : rows) {
+            String city = RegionUtil.normalizeCityName(r.getCity());
+            if (city == null || city.isEmpty()) city = "鏈煡";
+            double[] a = map.computeIfAbsent(city, k -> new double[6]);
+            a[0] += nz(r.getPassengerVolume());
+            a[1] += nz(r.getTurnover());
+            a[2] += nz(r.getPassengerCity());
+            a[3] += nz(r.getTurnoverCity());
+            if (r.getAvgDistance() != null && r.getAvgDistance() > 0) a[4] = r.getAvgDistance();
+            if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity();
+        }
+        for (double[] a : map.values()) {
+            if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2);
+            if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2);
+        }
+        return map;
+    }
+
+    /** 璇诲彇 docs/鍩庡競瀹㈣繍/銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬶細鎸� period(yyyy-MM) 琛屽彇 pin(B-E) 涓� 17 甯傚窞璁㈠崟(G-W) */
+    private Map<String, Object> loadMonthlyInput(String period) throws Exception {
+        File f = findInput();
+        try (InputStream in = new FileInputStream(f);
+             Workbook wb = WorkbookFactory.create(in)) {
+            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(0);
+            DataFormatter df = new DataFormatter();
+            List<String> cities = RegionUtil.cityList();
+            int n = cities.size();
+            for (int i = 1; i <= sh.getLastRowNum(); i++) {
+                Row row = sh.getRow(i);
+                if (row == null) continue;
+                Cell c0 = row.getCell(0);
+                if (c0 == null) continue;
+                String m = df.formatCellValue(c0).trim();
+                if (m.isEmpty()) continue;
+                if (m.length() > 7) m = m.substring(0, 7);
+                if (!period.equals(m)) continue;
+                Map<String, Object> map = new LinkedHashMap<>();
+                map.put("pinK", cellNum(row, 1, df, 0.0));
+                map.put("pinCK", cellNum(row, 2, df, 0.0));
+                map.put("pinZ", cellNum(row, 3, df, 0.0));
+                map.put("pinCZ", cellNum(row, 4, df, 0.0));
+                double[] orders = new double[n];
+                double orderSum = 0.0;
+                for (int k = 0; k < n; k++) {
+                    orders[k] = cellNum(row, 6 + k, df, 0.0);
+                    orderSum += orders[k];
+                }
+                if (orderSum <= 0) orderSum = cellNum(row, 5, df, 0.0);
+                map.put("orders", orders);
+                map.put("orderSum", orderSum);
+                return map;
+            }
+            return null;
+        }
+    }
+
+    private File findInput() throws Exception {
+        String rel = templateDir;
+        while (rel.startsWith("./")) rel = rel.substring(2);
+        String[] roots = {
+            templateDir,
+            System.getProperty("user.dir") + "/" + rel,
+            System.getProperty("user.dir") + "/../" + rel
+        };
+        for (String root : roots) {
+            if (root == null || root.trim().isEmpty()) continue;
+            for (String name : INPUT_CANDIDATES) {
+                File f = new File(root, name);
+                if (f.exists() && f.isFile()) return f;
+            }
+        }
+        throw new RuntimeException("鏈壘鍒般�婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬶紙鍚� 妯℃澘_ 鍓嶇紑鍛藉悕锛夛紝璇锋斁鍒� docs/鍩庡競瀹㈣繍 鐩綍");
+    }
+
+    private double cellNum(Row row, int idx, DataFormatter df, double def) {
+        Cell c = row.getCell(idx);
+        if (c == null) return def;
+        if (c.getCellType() == CellType.NUMERIC) return c.getNumericCellValue();
+        String s = df.formatCellValue(c).replace(",", "").trim();
+        if (s.isEmpty()) return def;
+        try {
+            return Double.parseDouble(s);
+        } catch (NumberFormatException e) {
+            return def;
+        }
+    }
+
+    private double nz(Double v) { return v == null ? 0.0 : v; }
+
+    private double round(double v, int scale) {
+        double factor = Math.pow(10, scale);
+        return Math.round(v * factor) / factor;
+    }
+}

--
Gitblit v1.9.1