UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

280 lines (238 loc) 9.17 kB
# Lonely Repeat Statement Pattern - AL Code Examples ## Overview The "Lonely Repeat Statement Pattern" refers to repeat-until loops that are difficult to read due to poor formatting, unclear conditions, or lack of proper structure. This sample demonstrates how to identify and fix these patterns in AL code. ## Problem Pattern: Lonely and Hard-to-Read Repeat Statements ### Example 1: Basic Formatting Issues **Problematic Code:** ```al procedure ProcessCustomerRecords() var Customer: Record Customer; Counter: Integer; begin Customer.FindFirst(); repeat Customer.CalcFields("Sales (LCY)"); if Customer."Sales (LCY)" > 10000 then begin Counter += 1; Message('High value customer: %1', Customer.Name); end; until Customer.Next() = 0; end; ``` **Improved Code:** ```al procedure ProcessCustomerRecords() var Customer: Record Customer; Counter: Integer; begin if Customer.FindFirst() then repeat Customer.CalcFields("Sales (LCY)"); if Customer."Sales (LCY)" > 10000 then begin Counter += 1; Message('High value customer: %1', Customer.Name); end; until Customer.Next() = 0; end; ``` ### Example 2: Complex Condition Handling **Problematic Code:** ```al procedure ValidateItemInventory() var Item: Record Item; InventorySetup: Record "Inventory Setup"; IsValid: Boolean; begin Item.SetRange(Blocked, false); Item.FindFirst(); repeat IsValid := true; Item.CalcFields(Inventory); if (Item.Inventory < Item."Reorder Point") and (Item."Reorder Point" > 0) and (Item."Replenishment System" = Item."Replenishment System"::Purchase) then IsValid := false; if not IsValid then Error('Item %1 needs reordering', Item."No."); until Item.Next() = 0; end; ``` **Improved Code:** ```al procedure ValidateItemInventory() var Item: Record Item; NeedsReordering: Boolean; begin Item.SetRange(Blocked, false); if Item.FindFirst() then repeat Item.CalcFields(Inventory); NeedsReordering := (Item.Inventory < Item."Reorder Point") and (Item."Reorder Point" > 0) and (Item."Replenishment System" = Item."Replenishment System"::Purchase); if NeedsReordering then Error('Item %1 needs reordering', Item."No."); until Item.Next() = 0; end; ``` ## Advanced Pattern Improvements ### Example 3: Nested Loop Clarity **Problematic Code:** ```al procedure RecalculateCustomerStatistics() var Customer: Record Customer; SalesLine: Record "Sales Line"; TotalAmount: Decimal; begin Customer.FindFirst(); repeat TotalAmount := 0; SalesLine.SetRange("Sell-to Customer No.", Customer."No."); if SalesLine.FindFirst() then repeat TotalAmount += SalesLine."Line Amount"; until SalesLine.Next() = 0; Customer."Sales (LCY)" := TotalAmount; Customer.Modify(); until Customer.Next() = 0; end; ``` **Improved Code:** ```al procedure RecalculateCustomerStatistics() var Customer: Record Customer; SalesLine: Record "Sales Line"; TotalAmount: Decimal; begin if Customer.FindFirst() then repeat TotalAmount := 0; SalesLine.SetRange("Sell-to Customer No.", Customer."No."); if SalesLine.FindFirst() then repeat TotalAmount += SalesLine."Line Amount"; until SalesLine.Next() = 0; Customer."Sales (LCY)" := TotalAmount; Customer.Modify(); until Customer.Next() = 0; end; ``` ### Example 4: Error Handling in Repeat Loops **Problematic Code:** ```al procedure PostSalesDocuments() var SalesHeader: Record "Sales Header"; SalesPost: Codeunit "Sales-Post"; begin SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order); SalesHeader.SetRange(Status, SalesHeader.Status::Released); SalesHeader.FindFirst(); repeat if not SalesPost.Run(SalesHeader) then Message('Error posting document %1', SalesHeader."No."); until SalesHeader.Next() = 0; end; ``` **Improved Code:** ```al procedure PostSalesDocuments() var SalesHeader: Record "Sales Header"; SalesPost: Codeunit "Sales-Post"; DocumentsProcessed: Integer; DocumentsFailed: Integer; begin SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order); SalesHeader.SetRange(Status, SalesHeader.Status::Released); if SalesHeader.FindFirst() then repeat Clear(SalesPost); if SalesPost.Run(SalesHeader) then DocumentsProcessed += 1 else begin DocumentsFailed += 1; Message('Error posting document %1: %2', SalesHeader."No.", GetLastErrorText()); end; until SalesHeader.Next() = 0; Message('Processing complete. Posted: %1, Failed: %2', DocumentsProcessed, DocumentsFailed); end; ``` ## Performance and Safety Patterns ### Example 5: Large Dataset Processing **Problematic Code:** ```al procedure ArchiveOldEntries() var GLEntry: Record "G/L Entry"; ArchiveEntry: Record "G/L Entry Archive"; begin GLEntry.SetFilter("Posting Date", '<%1', CalcDate('-2Y', Today())); GLEntry.FindFirst(); repeat ArchiveEntry.TransferFields(GLEntry); ArchiveEntry.Insert(); GLEntry.Delete(); until GLEntry.Next() = 0; end; ``` **Improved Code:** ```al procedure ArchiveOldEntries() var GLEntry: Record "G/L Entry"; ArchiveEntry: Record "G/L Entry Archive"; ProcessedCount: Integer; BatchSize: Integer; begin BatchSize := 1000; // Process in batches for better performance ProcessedCount := 0; GLEntry.SetFilter("Posting Date", '<%1', CalcDate('-2Y', Today())); if GLEntry.FindFirst() then repeat ArchiveEntry.TransferFields(GLEntry); ArchiveEntry.Insert(); GLEntry.Delete(); ProcessedCount += 1; // Commit periodically to avoid long transactions if ProcessedCount mod BatchSize = 0 then begin Commit(); Message('Processed %1 entries...', ProcessedCount); end; until GLEntry.Next() = 0; Message('Archive complete. Total entries processed: %1', ProcessedCount); end; ``` ### Example 6: Conditional Processing with Early Exit **Problematic Code:** ```al procedure FindFirstAvailableItem(LocationCode: Code[10]): Code[20] var Item: Record Item; ItemLedgerEntry: Record "Item Ledger Entry"; AvailableQty: Decimal; begin Item.FindFirst(); repeat Item.SetRange("Location Filter", LocationCode); Item.CalcFields(Inventory); if Item.Inventory > 0 then exit(Item."No."); until Item.Next() = 0; end; ``` **Improved Code:** ```al procedure FindFirstAvailableItem(LocationCode: Code[10]): Code[20] var Item: Record Item; begin Item.SetRange(Blocked, false); Item.SetRange(Type, Item.Type::Inventory); if Item.FindFirst() then repeat Item.SetRange("Location Filter", LocationCode); Item.CalcFields(Inventory); if Item.Inventory > 0 then exit(Item."No."); until Item.Next() = 0; // Return empty if no available item found exit(''); end; ``` ## Best Practices Summary ### Formatting Guidelines 1. **Always use proper indentation** for repeat-until blocks 2. **Place conditions on separate lines** for complex boolean expressions 3. **Use meaningful variable names** instead of inline calculations 4. **Add blank lines** to separate logical sections within loops ### Performance Considerations 1. **Process in batches** for large datasets to avoid transaction timeouts 2. **Use proper filters** before starting repeat loops to minimize iterations 3. **Calculate fields only when necessary** within the loop 4. **Consider using FindSet()** with ReadOnly parameter for read-only operations ### Error Handling 1. **Always check FindFirst() result** before starting repeat loop 2. **Handle errors gracefully** with appropriate user feedback 3. **Use Clear()** on codeunit variables before calling Run() 4. **Implement progress indicators** for long-running operations ### Code Readability 1. **Extract complex conditions** into well-named boolean variables 2. **Use early exit patterns** when searching for specific records 3. **Add comments** to explain business logic within loops 4. **Avoid deeply nested structures** by using helper procedures ## Related Patterns - **FindSet Pattern**: Use `FindSet()` with `ReadOnly` parameter for better performance when only reading data - **Batch Processing Pattern**: Process records in controlled batches with periodic commits - **Error Accumulation Pattern**: Collect errors during processing and report them together - **Progress Indication Pattern**: Provide user feedback during long-running repeat operations