| | |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | /** |
| | | * The Excel comparison base column (C) is an operator backup value only: it must never change the |
| | | * national/provincial ring ratio. This locks in that behaviour. |
| | | * Ring-ratio denominators are built one day at a time. The Excel comparison base (column C) is |
| | | * consumed on the source day and inherited by the next day's denominator, while historical |
| | | * adjustment records change only that historical day's same-day value. |
| | | */ |
| | | class HolidayComparisonBaseTest { |
| | | |
| | | @Test |
| | | void ringRatioUsesPreviousDayEvenWhenComparisonBaseIsStored() throws Exception { |
| | | void comparisonBaseChangesOnlyTheNextDayRingRatioDenominator() throws Exception { |
| | | HolidayStatService service = serviceWithData( |
| | | 2026, |
| | | passengerRows(2026, "50", "10", "6", "3"), |
| | | vehicleRows(2026), |
| | | Collections.<HolidayVehicleAdjustment>emptyList()); |
| | | |
| | | List<Map<String, Object>> rows = service.buildOverviewRows( |
| | | 2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2026)); |
| | | |
| | | assertEquals(new BigDecimal("323.7837"), rows.get(2).get("nationalProvincial")); |
| | | assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase")); |
| | | assertEquals(new BigDecimal("322.7695"), rows.get(3).get("nationalProvincial")); |
| | | |
| | | BigDecimal nationalRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio"); |
| | | assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), nationalRatio); |
| | | assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), nationalRatio); |
| | | |
| | | HolidayStatService.DailyMetrics base = service.ringRatioBaseMetrics( |
| | | 2026, HolidayConstants.TYPE_NATIONAL_DAY, 4); |
| | | assertNotNull(base); |
| | | assertEquals(new BigDecimal("320.2418"), base.getNationalProvincial()); |
| | | assertDecimalEquals("1008.58032", base.getNonBusinessPersonFlow()); |
| | | assertDecimalEquals("1058.58032", base.getRoadPersonFlow()); |
| | | assertDecimalEquals("1077.58032", base.getTotalPersonFlow()); |
| | | } |
| | | |
| | | @Test |
| | | void historicalAdjustmentChangesTheHistoricalSameDayValue() throws Exception { |
| | | HolidayVehicleAdjustment adjustment = new HolidayVehicleAdjustment(); |
| | | adjustment.setYear(2025); |
| | | adjustment.setFlowDate(LocalDate.of(2025, 10, 4)); |
| | | adjustment.setAdjustedValue(new BigDecimal("337.5009")); |
| | | |
| | | HolidayStatService service = serviceWithData( |
| | | 2025, |
| | | passengerRows(2025, "50", "10", "6", "3"), |
| | | Arrays.asList( |
| | | vehicle("2025-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null), |
| | | vehicle("2025-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "362.0035", null), |
| | | vehicle("2025-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null), |
| | | vehicle("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "350.0000", null)), |
| | | Collections.singletonList(adjustment)); |
| | | |
| | | List<Map<String, Object>> rows = service.buildOverviewRows( |
| | | 2025, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2025)); |
| | | |
| | | assertEquals(new BigDecimal("337.5009"), rows.get(3).get("nationalProvincial")); |
| | | } |
| | | |
| | | private static HolidayStatService serviceWithData(int year, |
| | | List<HolidayPassengerFlow> passengerRows, |
| | | List<HolidayVehicleFlow> vehicleRows, |
| | | List<HolidayVehicleAdjustment> adjustments) throws Exception { |
| | | HolidayConfigService configService = mock(HolidayConfigService.class); |
| | | HolidayPassengerFlowMapper passengerMapper = mock(HolidayPassengerFlowMapper.class); |
| | | HolidayVehicleFlowMapper vehicleMapper = mock(HolidayVehicleFlowMapper.class); |
| | | HolidayVehicleAdjustmentMapper adjustmentMapper = mock(HolidayVehicleAdjustmentMapper.class); |
| | | |
| | | when(configService.coefficientOf(2026, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4")); |
| | | when(passengerMapper.selectList(any())).thenReturn(new ArrayList<HolidayPassengerFlow>()); |
| | | when(vehicleMapper.selectList(any())).thenReturn(vehicleRows()); |
| | | when(configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4")); |
| | | when(configService.findHolidayDays(year, HolidayConstants.TYPE_NATIONAL_DAY)).thenReturn(calendar(year)); |
| | | when(passengerMapper.selectList(any())).thenReturn(passengerRows); |
| | | when(vehicleMapper.selectList(any())).thenReturn(vehicleRows); |
| | | when(adjustmentMapper.selectList(any())).thenReturn(adjustments); |
| | | |
| | | HolidayStatService service = new HolidayStatService(); |
| | | inject(service, "configService", configService); |
| | | inject(service, "passengerFlowMapper", passengerMapper); |
| | | inject(service, "vehicleFlowMapper", vehicleMapper); |
| | | |
| | | List<Map<String, Object>> rows = service.buildOverviewRows(2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar()); |
| | | |
| | | BigDecimal ringRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio"); |
| | | assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), ringRatio); |
| | | assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), ringRatio); |
| | | assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase")); |
| | | } |
| | | |
| | | @Test |
| | | void exportFormulaComparesPreviousRowInsteadOfComparisonBaseColumn() { |
| | | assertEquals("=(D6-D5)/D5", HolidayWorkbookService.ringRatioFormula(6, 5)); |
| | | inject(service, "vehicleAdjustmentMapper", adjustmentMapper); |
| | | return service; |
| | | } |
| | | |
| | | private static BigDecimal ratio(BigDecimal current, BigDecimal base) { |
| | | return HolidayNumberUtils.normalize(HolidayNumberUtils.divide(current, base).subtract(BigDecimal.ONE)); |
| | | } |
| | | |
| | | private static List<HolidayVehicleFlow> vehicleRows() { |
| | | private static void assertDecimalEquals(String expected, BigDecimal actual) { |
| | | assertNotNull(actual); |
| | | assertEquals(0, new BigDecimal(expected).compareTo(actual)); |
| | | } |
| | | |
| | | private static List<HolidayVehicleFlow> vehicleRows(int year) { |
| | | List<HolidayVehicleFlow> rows = new ArrayList<>(); |
| | | rows.add(vehicle("2026-10-03", "323.7837", "320.2418")); |
| | | rows.add(vehicle("2026-10-04", "322.7695", null)); |
| | | rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null)); |
| | | rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "323.7837", "320.2418")); |
| | | rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null)); |
| | | rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "322.7695", null)); |
| | | return rows; |
| | | } |
| | | |
| | | private static HolidayVehicleFlow vehicle(String date, String value, String base) { |
| | | private static HolidayVehicleFlow vehicle(String date, String roadCategory, String value, String base) { |
| | | HolidayVehicleFlow row = new HolidayVehicleFlow(); |
| | | row.setYear(2026); |
| | | row.setYear(LocalDate.parse(date).getYear()); |
| | | row.setFlowDate(LocalDate.parse(date)); |
| | | row.setRoadCategory(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); |
| | | row.setRoadCategory(roadCategory); |
| | | row.setVehicleType(HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER); |
| | | row.setVehicleCount(new BigDecimal(value)); |
| | | row.setComparisonBase(base == null ? null : new BigDecimal(base)); |
| | | return row; |
| | | } |
| | | |
| | | private static List<HolidayCalendar> calendar() { |
| | | private static List<HolidayPassengerFlow> passengerRows(int year, String road, String rail, |
| | | String civilAviation, String water) { |
| | | List<HolidayPassengerFlow> rows = new ArrayList<>(); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_ROAD, road)); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_RAIL, rail)); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation)); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_WATER, water)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_ROAD, road)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_RAIL, rail)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_WATER, water)); |
| | | return rows; |
| | | } |
| | | |
| | | private static HolidayPassengerFlow passenger(String date, String mode, String value) { |
| | | HolidayPassengerFlow row = new HolidayPassengerFlow(); |
| | | row.setYear(LocalDate.parse(date).getYear()); |
| | | row.setFlowDate(LocalDate.parse(date)); |
| | | row.setTransportMode(mode); |
| | | row.setPassengerCount(new BigDecimal(value)); |
| | | return row; |
| | | } |
| | | |
| | | private static List<HolidayCalendar> calendar(int year) { |
| | | List<HolidayCalendar> days = new ArrayList<>(); |
| | | for (int i = 1; i <= 4; i++) { |
| | | HolidayCalendar day = new HolidayCalendar(); |
| | | day.setYear(2026); |
| | | day.setYear(year); |
| | | day.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); |
| | | day.setDayIndex(i); |
| | | day.setHolidayDate(LocalDate.of(2026, 10, i)); |
| | | day.setHolidayDate(LocalDate.of(year, 10, i)); |
| | | days.add(day); |
| | | } |
| | | return days; |