|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +/** |
| 6 | + * Example 01 — String Processing: progressive optimization. |
| 7 | + * |
| 8 | + * Simulates building an HTML report from 5,000 records. |
| 9 | + * Run 3 times with increasing iteration number to see SCI drop: |
| 10 | + * |
| 11 | + * php 01-string-processing.php 1 ← naive: .= in loop |
| 12 | + * php 01-string-processing.php 2 ← fix: array + implode |
| 13 | + * php 01-string-processing.php 3 ← refined: sprintf + single-pass stats |
| 14 | + * |
| 15 | + * @author fullo <https://github.com/fullo> |
| 16 | + * @license MIT |
| 17 | + * @version 1.0 |
| 18 | + */ |
| 19 | + |
| 20 | +$iteration = (int) ($argv[1] ?? 1); |
| 21 | +echo "=== String Processing — iteration {$iteration}/3 ===\n"; |
| 22 | + |
| 23 | +// ── Generate 5,000 user records (same seed for all iterations) ── |
| 24 | +mt_srand(42); |
| 25 | +$users = []; |
| 26 | +for ($i = 0; $i < 5000; $i++) { |
| 27 | + $users[] = [ |
| 28 | + 'id' => $i + 1, |
| 29 | + 'name' => 'User ' . str_pad((string) ($i + 1), 4, '0', STR_PAD_LEFT), |
| 30 | + 'email' => 'user' . ($i + 1) . '@example.com', |
| 31 | + 'score' => mt_rand(0, 10000) / 100, |
| 32 | + 'active' => $i % 7 !== 0, |
| 33 | + ]; |
| 34 | +} |
| 35 | + |
| 36 | +$header = '<!DOCTYPE html><html><head><title>User Report</title>' |
| 37 | + . '<style>table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:4px 8px}' |
| 38 | + . 'tr:nth-child(even){background:#f9f9f9}.inactive{color:#999}</style></head><body>' |
| 39 | + . '<h1>User Report</h1><p>Generated: ' . date('Y-m-d H:i:s') . '</p>' |
| 40 | + . '<table><thead><tr><th>ID</th><th>Name</th><th>Email</th><th>Score</th><th>Status</th></tr></thead><tbody>'; |
| 41 | + |
| 42 | +$footer = '</tbody></table>'; |
| 43 | + |
| 44 | +match ($iteration) { |
| 45 | + // ── Iteration 1: Naive — string concatenation in a loop ── |
| 46 | + // Each .= copies the entire $html string (O(n²) memory operations). |
| 47 | + 1 => (function () use ($users, $header, $footer): string { |
| 48 | + $html = $header; |
| 49 | + |
| 50 | + foreach ($users as $user) { |
| 51 | + $class = $user['active'] ? '' : ' class="inactive"'; |
| 52 | + $status = $user['active'] ? 'Active' : 'Inactive'; |
| 53 | + $html .= '<tr' . $class . '>'; |
| 54 | + $html .= '<td>' . $user['id'] . '</td>'; |
| 55 | + $html .= '<td>' . htmlspecialchars($user['name']) . '</td>'; |
| 56 | + $html .= '<td>' . htmlspecialchars($user['email']) . '</td>'; |
| 57 | + $html .= '<td>' . number_format($user['score'], 2) . '</td>'; |
| 58 | + $html .= '<td>' . $status . '</td>'; |
| 59 | + $html .= '</tr>'; |
| 60 | + } |
| 61 | + |
| 62 | + $html .= $footer; |
| 63 | + |
| 64 | + // Summary: second loop over all users |
| 65 | + $active = 0; |
| 66 | + $total = 0.0; |
| 67 | + foreach ($users as $user) { |
| 68 | + if ($user['active']) { |
| 69 | + $active++; |
| 70 | + } |
| 71 | + $total += $user['score']; |
| 72 | + } |
| 73 | + $html .= '<p>Active: ' . $active . '/' . count($users) . '</p>'; |
| 74 | + $html .= '<p>Avg score: ' . number_format($total / count($users), 2) . '</p>'; |
| 75 | + $html .= '</body></html>'; |
| 76 | + |
| 77 | + echo 'Output: ' . strlen($html) . " bytes | Active: {$active}\n"; |
| 78 | + return $html; |
| 79 | + })(), |
| 80 | + |
| 81 | + // ── Iteration 2: Fix — array + implode, single allocation ── |
| 82 | + // Each $parts[] = '...' is O(1). implode() does one allocation at the end. |
| 83 | + 2 => (function () use ($users, $header, $footer): string { |
| 84 | + $parts = [$header]; |
| 85 | + |
| 86 | + foreach ($users as $user) { |
| 87 | + $class = $user['active'] ? '' : ' class="inactive"'; |
| 88 | + $status = $user['active'] ? 'Active' : 'Inactive'; |
| 89 | + $parts[] = '<tr' . $class . '>' |
| 90 | + . '<td>' . $user['id'] . '</td>' |
| 91 | + . '<td>' . htmlspecialchars($user['name']) . '</td>' |
| 92 | + . '<td>' . htmlspecialchars($user['email']) . '</td>' |
| 93 | + . '<td>' . number_format($user['score'], 2) . '</td>' |
| 94 | + . '<td>' . $status . '</td>' |
| 95 | + . '</tr>'; |
| 96 | + } |
| 97 | + |
| 98 | + $parts[] = $footer; |
| 99 | + |
| 100 | + // Summary: still a second loop |
| 101 | + $active = 0; |
| 102 | + $total = 0.0; |
| 103 | + foreach ($users as $user) { |
| 104 | + if ($user['active']) { |
| 105 | + $active++; |
| 106 | + } |
| 107 | + $total += $user['score']; |
| 108 | + } |
| 109 | + $parts[] = '<p>Active: ' . $active . '/' . count($users) . '</p>'; |
| 110 | + $parts[] = '<p>Avg score: ' . number_format($total / count($users), 2) . '</p>'; |
| 111 | + $parts[] = '</body></html>'; |
| 112 | + |
| 113 | + $html = implode('', $parts); |
| 114 | + echo 'Output: ' . strlen($html) . " bytes | Active: {$active}\n"; |
| 115 | + return $html; |
| 116 | + })(), |
| 117 | + |
| 118 | + // ── Iteration 3: Refined — sprintf rows + single-pass stats ── |
| 119 | + // All stats computed inline during the same loop. No second iteration. |
| 120 | + // sprintf for each row avoids intermediate concatenation. |
| 121 | + 3 => (function () use ($users, $header, $footer): string { |
| 122 | + $parts = [$header]; |
| 123 | + $active = 0; |
| 124 | + $total = 0.0; |
| 125 | + |
| 126 | + foreach ($users as $user) { |
| 127 | + $parts[] = sprintf( |
| 128 | + '<tr%s><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>', |
| 129 | + $user['active'] ? '' : ' class="inactive"', |
| 130 | + $user['id'], |
| 131 | + htmlspecialchars($user['name']), |
| 132 | + htmlspecialchars($user['email']), |
| 133 | + number_format($user['score'], 2), |
| 134 | + $user['active'] ? 'Active' : 'Inactive', |
| 135 | + ); |
| 136 | + |
| 137 | + if ($user['active']) { |
| 138 | + $active++; |
| 139 | + } |
| 140 | + $total += $user['score']; |
| 141 | + } |
| 142 | + |
| 143 | + $parts[] = $footer; |
| 144 | + $parts[] = '<p>Active: ' . $active . '/' . count($users) . '</p>'; |
| 145 | + $parts[] = '<p>Avg score: ' . number_format($total / count($users), 2) . '</p>'; |
| 146 | + $parts[] = '</body></html>'; |
| 147 | + |
| 148 | + $html = implode('', $parts); |
| 149 | + echo 'Output: ' . strlen($html) . " bytes | Active: {$active}\n"; |
| 150 | + return $html; |
| 151 | + })(), |
| 152 | + |
| 153 | + default => throw new InvalidArgumentException("Usage: php 01-string-processing.php [1|2|3]"), |
| 154 | +}; |
0 commit comments