# Design Document ## Overview This design document specifies the modifications to the `GetYearlyStatisticsByMonthAsync` method to implement a rolling 15-month display window that adapts based on the current month. The solution maintains backward compatibility with the existing API while providing more contextually relevant data to users. ## Architecture The solution follows the existing service architecture pattern: ``` SecondaryCircuitInspectionResultStatisticsAppService └── GetYearlyStatisticsByMonthAsync() ├── CalculateDisplayWindow() [NEW] ├── GetStatisticsAsync() [EXISTING] ├── PredictMonthlyStatisticsWithModuleDetails() [EXISTING] └── CreateZeroStatistics() [NEW] ``` The design introduces two new helper methods and modifies the main method logic to support the rolling window calculation. ## Components and Interfaces ### Modified Component: GetYearlyStatisticsByMonthAsync **Signature:** ```csharp public async Task> GetYearlyStatisticsByMonthAsync( int year, CancellationToken cancellationToken = default) ``` **Behavior Changes:** - The `year` parameter is now ignored; the method always uses the current year - Returns a 15-month rolling window instead of a fixed calendar year - Includes cross-year data when appropriate ### New Component: CalculateDisplayWindow **Purpose:** Calculate the start and end months for the display window based on the current month. **Signature:** ```csharp private (int startYear, int startMonth, int endYear, int endMonth) CalculateDisplayWindow( int currentYear, int currentMonth) ``` **Algorithm:** ``` Input: currentYear, currentMonth Output: (startYear, startMonth, endYear, endMonth) 1. Calculate historical start (3 months before current): historicalStartMonth = currentMonth - 3 2. Handle year boundary for historical data: IF historicalStartMonth <= 0 THEN startYear = currentYear - 1 startMonth = 12 + historicalStartMonth ELSE startYear = currentYear startMonth = historicalStartMonth END IF 3. Calculate end month (11 months after current): endMonthOffset = currentMonth + 11 4. Handle year boundary for end data: IF endMonthOffset > 12 THEN endYear = currentYear + 1 endMonth = endMonthOffset - 12 ELSE endYear = currentYear endMonth = endMonthOffset END IF 5. Return (startYear, startMonth, endYear, endMonth) ``` **Examples:** - Current: September 2025 → Window: June 2025 to April 2026 (but display Jan-Dec 2025 per requirements) - Current: October 2025 → Window: July 2025 to August 2026 (but display Jan 2025 to Jan 2026 per requirements) - Current: January 2025 → Window: October 2024 to November 2025 (but display Oct-Dec 2024, Jan-Dec 2025 per requirements) ### New Component: CreateZeroStatistics **Purpose:** Create a statistics object with all counts set to zero for months beyond the prediction range. **Signature:** ```csharp private SecondaryCircuitInspectionStatisticsOutput CreateZeroStatistics( int year, int month) ``` **Implementation:** ```csharp return new SecondaryCircuitInspectionStatisticsOutput { StatisticsStartTime = new DateTime(year, month, 1), StatisticsEndTime = new DateTime(year, month, 1).AddMonths(1).AddSeconds(-1), TotalCount = 0, NormalCount = 0, AbnormalCount = 0, ErrorCount = 0, AverageExecutionDurationMs = 0, AbnormalRate = 0, StatisticsByModule = new List() }; ``` ## Data Models ### Existing Models (No Changes) **YearlyStatisticsByMonthOutput:** ```csharp public class YearlyStatisticsByMonthOutput { public int Year { get; set; } public List MonthlyStatistics { get; set; } } ``` **MonthlyStatisticsItem:** ```csharp public class MonthlyStatisticsItem { public int Year { get; set; } public int Month { get; set; } public SecondaryCircuitInspectionStatisticsOutput Statistics { get; set; } public bool IsPredicted { get; set; } } ``` ### Data Flow ```mermaid graph TD A[GetYearlyStatisticsByMonthAsync] --> B[Get Current Date] B --> C[CalculateDisplayWindow] C --> D[Iterate Through Display Window] D --> E{Month Type?} E -->|Historical| F[GetStatisticsAsync] E -->|Current| F E -->|Predicted| G[PredictMonthlyStatisticsWithModuleDetails] E -->|Zero| H[CreateZeroStatistics] F --> I[Add to MonthlyStatistics] G --> I H --> I I --> J{More Months?} J -->|Yes| D J -->|No| K[Return Result] ``` ## Detailed Algorithm ### Main Method Logic ``` 1. Get current date and time: now = DateTime.Now currentYear = now.Year currentMonth = now.Month 2. Initialize output: output = new YearlyStatisticsByMonthOutput { Year = currentYear } 3. Calculate display window: (startYear, startMonth, endYear, endMonth) = CalculateDisplayWindow(currentYear, currentMonth) 4. Collect historical data for prediction: historicalMonthlyStats = new List<>() 5. Iterate through display window: FOR each month from (startYear, startMonth) to (endYear, endMonth): targetYear = current iteration year targetMonth = current iteration month a. Determine month type: isHistorical = (targetYear < currentYear) OR (targetYear == currentYear AND targetMonth <= currentMonth) isPredicted = (targetYear > currentYear) OR (targetYear == currentYear AND targetMonth > currentMonth AND targetMonth <= currentMonth + 3) isZero = NOT isHistorical AND NOT isPredicted b. Get statistics based on type: IF isHistorical THEN dateRange = $"{targetYear:D4}-{targetMonth:D2}" input = new SecondaryCircuitInspectionStatisticsInput { DateRange = dateRange } monthlyStats = await GetStatisticsAsync(input, cancellationToken) historicalMonthlyStats.Add(monthlyStats) ELSE IF isPredicted THEN IF predictedStats is empty THEN predictStartYear = currentYear predictStartMonth = currentMonth + 1 IF predictStartMonth > 12 THEN predictStartMonth = 1 predictStartYear++ END IF predictedStats = PredictMonthlyStatisticsWithModuleDetails( historicalMonthlyStats, predictStartYear, predictStartMonth, 3) END IF monthlyStats = predictedStats[predictIndex++] ELSE (isZero) monthlyStats = CreateZeroStatistics(targetYear, targetMonth) END IF c. Add to output: output.MonthlyStatistics.Add(new MonthlyStatisticsItem { Year = targetYear, Month = targetMonth, Statistics = monthlyStats, IsPredicted = isPredicted OR isZero }) END FOR 6. Return success result: return RequestResult.CreateSuccess(output) ``` ### Special Case Handling **September (Month 9):** ``` Current: September 2025 Historical: June, July, August 2025 Current: September 2025 Predicted: October, November, December 2025 Zero: None (only 12 months displayed: Jan-Dec 2025) Special logic: Display window is Jan-Dec of current year only ``` **October (Month 10):** ``` Current: October 2025 Historical: July, August, September 2025 Current: October 2025 Predicted: November, December 2025, January 2026 Zero: February-December 2026 Display: Jan 2025 - Jan 2026 (13 months) ``` **January (Month 1):** ``` Current: January 2025 Historical: October, November, December 2024 Current: January 2025 Predicted: February, March, April 2025 Zero: May-December 2025 Display: Oct-Dec 2024, Jan-Dec 2025 (15 months) ``` ## Correctness Properties *A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* ### Property 1: Three Historical Months Preceding Current *For any* current month from April through December, the display window should include the 3 months immediately preceding the current month with actual statistics data retrieved from the database. **Validates: Requirements 1.1, 1.5** ### Property 2: Current Month Inclusion and Marking *For any* current month, the display window should include that month, retrieve actual statistics data from the database, and mark it as historical (IsPredicted = false). **Validates: Requirements 2.1, 2.2, 2.3** ### Property 3: Three Predicted Months Following Current *For any* current month, the display window should include the 3 months immediately following the current month, generate predicted statistics using the prediction algorithm, mark them as predicted (IsPredicted = true), and correctly handle year boundaries. **Validates: Requirements 3.1, 3.2, 3.3, 3.4** ### Property 4: Zero Data Beyond Prediction Range *For any* month in the display window beyond the 3-month prediction range (current + 4 onwards), all count values (TotalCount, NormalCount, AbnormalCount, ErrorCount) should equal 0, IsPredicted should be false, and StatisticsByModule should be an empty list. **Validates: Requirements 4.1, 4.3, 4.4** ### Property 5: Display Window Size *For any* current month except September, the display window should contain exactly 15 months; for September, the display window should contain exactly 12 months. **Validates: Requirements 10.2, 12.2** ### Property 6: Chronological Month Sequence *For any* two consecutive MonthlyStatisticsItem objects in the output, the second month should be exactly one calendar month after the first, with year values correctly incremented when crossing year boundaries. **Validates: Requirements 11.3, 12.4** ### Property 7: Month and Year Value Validity *For any* MonthlyStatisticsItem in the output, the Month property should be in the range 1-12 inclusive, and the Year property should be a valid four-digit year (>= 1900 and <= 9999). **Validates: Requirements 11.1, 11.2** ### Property 8: Output Year Property *For any* successful response, the Year property of YearlyStatisticsByMonthOutput should equal the current year. **Validates: Requirements 12.5** ## Error Handling ### Input Validation ```csharp // Year parameter validation (maintained for backward compatibility) if (year < 1900 || year > 9999) { return RequestResult.CreateFailed( $"年份参数无效: {year}。有效范围为 1900-9999"); } // Note: The year parameter is validated but not used in calculations Log4Helper.Info($"Year parameter {year} provided but using current year {currentYear} for rolling window"); ``` ### Exception Handling ```csharp try { // Main logic } catch (Exception ex) { Log4Helper.Error($"获取年度统计信息失败: Year={year}, Error={ex.Message}", ex); return RequestResult.CreateFailed( $"获取年度统计信息失败: {ex.Message}"); } ``` ### Partial Failure Handling ```csharp // If a specific month fails to load, add zero statistics instead of failing entirely try { monthlyStats = await GetStatisticsAsync(input, cancellationToken); } catch (Exception ex) { Log4Helper.Warning($"获取月度统计失败: Year={targetYear}, Month={targetMonth}, Error={ex.Message}"); monthlyStats = CreateZeroStatistics(targetYear, targetMonth); } ``` ## Testing Strategy ### Unit Testing Unit tests will verify specific examples and edge cases: 1. **Test September Display**: Verify that September returns exactly 12 months (Jan-Dec of current year) 2. **Test October Display**: Verify that October returns 13 months (Jan of current year to Jan of next year) 3. **Test January Display**: Verify that January returns 15 months (Oct-Dec of previous year, Jan-Dec of current year) 4. **Test Year Boundary**: Verify correct year values when crossing year boundaries 5. **Test Month Sequence**: Verify months are in correct chronological order 6. **Test Zero Data**: Verify months beyond prediction range have all zeros 7. **Test IsPredicted Flag**: Verify historical, predicted, and zero months have correct flags ### Property-Based Testing Property-based tests will verify universal properties across all inputs using a PBT library (e.g., FsCheck for C#, or a similar library): 1. **Property Test for Display Window Size**: Generate random current months, verify window size is correct 2. **Property Test for Historical Data**: Generate random current months, verify all months <= current are historical 3. **Property Test for Predicted Data**: Generate random current months, verify next 3 months are predicted 4. **Property Test for Zero Data**: Generate random current months, verify months beyond prediction are zero 5. **Property Test for Chronological Order**: Generate random current months, verify all months are sequential 6. **Property Test for Month Validity**: Generate random current months, verify all month numbers are 1-12 7. **Property Test for Year Validity**: Generate random current months, verify year values are valid ### Integration Testing Integration tests will verify the method works correctly with real dependencies: 1. **Test with Real Database**: Verify historical data is correctly retrieved 2. **Test with Prediction Algorithm**: Verify predicted data is correctly generated 3. **Test End-to-End**: Verify complete flow from request to response ### Test Configuration - Minimum 100 iterations per property test - Each property test tagged with: **Feature: yearly-statistics-display-logic, Property {number}: {property_text}** - Unit tests and property tests are complementary (both required)