@@ -0,0 +1,588 @@ |
| 1 | +package llm |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "math" |
| 6 | + "time" |
| 7 | +) |
| 8 | + |
| 9 | +// BenchmarkSample represents a real command failure with expected outputs |
| 10 | +type BenchmarkSample struct { |
| 11 | + ID string |
| 12 | + Command string |
| 13 | + ExitCode int |
| 14 | + Stderr string |
| 15 | + Context SmartFallbackContext |
| 16 | + Category string // "git", "npm", "docker", etc. |
| 17 | + Description string |
| 18 | + GoldInsults []string // Human-written example insults |
| 19 | + Tags []string // Expected tags for this scenario |
| 20 | +} |
| 21 | + |
| 22 | +// BenchmarkResults contains evaluation metrics |
| 23 | +type BenchmarkResults struct { |
| 24 | + SystemName string |
| 25 | + TotalSamples int |
| 26 | + AvgRelevance float64 |
| 27 | + AvgLatency time.Duration |
| 28 | + AvgConfidence float64 |
| 29 | + DiversityScore float64 |
| 30 | + FallbackRate float64 |
| 31 | + MemoryUsageKB int |
| 32 | + DetailedScores []SampleScore |
| 33 | +} |
| 34 | + |
| 35 | +// SampleScore contains per-sample evaluation |
| 36 | +type SampleScore struct { |
| 37 | + SampleID string |
| 38 | + GeneratedInsult string |
| 39 | + Relevance float64 // 0-1: How relevant to the error |
| 40 | + Latency time.Duration |
| 41 | + Confidence float64 |
| 42 | + NoveltyScore float64 |
| 43 | + Method string // "semantic", "tag", "markov", "ensemble" |
| 44 | +} |
| 45 | + |
| 46 | +// Benchmark framework for systematic evaluation |
| 47 | +type Benchmark struct { |
| 48 | + Name string |
| 49 | + Samples []BenchmarkSample |
| 50 | +} |
| 51 | + |
| 52 | +// NewBenchmark creates a comprehensive benchmark dataset |
| 53 | +func NewBenchmark() *Benchmark { |
| 54 | + return &Benchmark{ |
| 55 | + Name: "Parrot Insult Quality Benchmark v1.0", |
| 56 | + Samples: createBenchmarkSamples(), |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +// createBenchmarkSamples creates a comprehensive test dataset |
| 61 | +func createBenchmarkSamples() []BenchmarkSample { |
| 62 | + samples := []BenchmarkSample{} |
| 63 | + |
| 64 | + // Git failures |
| 65 | + samples = append(samples, BenchmarkSample{ |
| 66 | + ID: "git-001", |
| 67 | + Command: "git push origin main", |
| 68 | + ExitCode: 1, |
| 69 | + Stderr: "error: failed to push some refs\nTo github.com:user/repo.git\n ! [rejected] main -> main (fetch first)", |
| 70 | + Context: SmartFallbackContext{ |
| 71 | + CommandType: "git", |
| 72 | + Command: "git", |
| 73 | + Subcommand: "push", |
| 74 | + GitBranch: "main", |
| 75 | + ErrorPattern: "permission_denied", |
| 76 | + IsRepeatedFailure: false, |
| 77 | + }, |
| 78 | + Category: "git", |
| 79 | + Description: "Git push rejected on main branch", |
| 80 | + GoldInsults: []string{ |
| 81 | + "Push rejected. Did you forget to pull first?", |
| 82 | + "The remote has standards. Your code doesn't meet them.", |
| 83 | + }, |
| 84 | + Tags: []string{"git", "push", "main_branch"}, |
| 85 | + }) |
| 86 | + |
| 87 | + samples = append(samples, BenchmarkSample{ |
| 88 | + ID: "git-002", |
| 89 | + Command: "git merge feature/new-ui", |
| 90 | + ExitCode: 1, |
| 91 | + Stderr: "CONFLICT (content): Merge conflict in src/app.js\nAutomatic merge failed; fix conflicts and then commit the result.", |
| 92 | + Context: SmartFallbackContext{ |
| 93 | + CommandType: "git", |
| 94 | + Command: "git", |
| 95 | + Subcommand: "merge", |
| 96 | + GitBranch: "main", |
| 97 | + ErrorPattern: "merge_conflict", |
| 98 | + IsRepeatedFailure: false, |
| 99 | + }, |
| 100 | + Category: "git", |
| 101 | + Description: "Merge conflict", |
| 102 | + GoldInsults: []string{ |
| 103 | + "Merge conflict. Maybe communicate with your team?", |
| 104 | + "<<<<<<< HEAD is not a valid merge resolution strategy", |
| 105 | + }, |
| 106 | + Tags: []string{"git", "merge", "merge_conflict"}, |
| 107 | + }) |
| 108 | + |
| 109 | + samples = append(samples, BenchmarkSample{ |
| 110 | + ID: "git-003", |
| 111 | + Command: "git push --force origin main", |
| 112 | + ExitCode: 1, |
| 113 | + Stderr: "error: refusing to update checked out branch: refs/heads/main", |
| 114 | + Context: SmartFallbackContext{ |
| 115 | + CommandType: "git", |
| 116 | + Command: "git", |
| 117 | + Subcommand: "push", |
| 118 | + GitBranch: "main", |
| 119 | + ErrorPattern: "permission_denied", |
| 120 | + IsRepeatedFailure: true, |
| 121 | + TimeOfDay: 2, |
| 122 | + }, |
| 123 | + Category: "git", |
| 124 | + Description: "Force push to main at 2 AM (repeated failure)", |
| 125 | + GoldInsults: []string{ |
| 126 | + "Force pushing to main at 2 AM? Bold strategy.", |
| 127 | + "--force won't force competence into you", |
| 128 | + }, |
| 129 | + Tags: []string{"git", "push", "main_branch", "late_night", "repeated"}, |
| 130 | + }) |
| 131 | + |
| 132 | + // NPM failures |
| 133 | + samples = append(samples, BenchmarkSample{ |
| 134 | + ID: "npm-001", |
| 135 | + Command: "npm install", |
| 136 | + ExitCode: 1, |
| 137 | + Stderr: "npm ERR! code ENOENT\nnpm ERR! syscall open\nnpm ERR! path /home/user/project/package.json\nnpm ERR! errno -2", |
| 138 | + Context: SmartFallbackContext{ |
| 139 | + CommandType: "nodejs", |
| 140 | + Command: "npm", |
| 141 | + Subcommand: "install", |
| 142 | + ProjectType: "node", |
| 143 | + ErrorPattern: "not_found", |
| 144 | + }, |
| 145 | + Category: "npm", |
| 146 | + Description: "Missing package.json", |
| 147 | + GoldInsults: []string{ |
| 148 | + "package.json not found. Neither is your organizational skill.", |
| 149 | + "Are you in the right directory? Rhetorical question.", |
| 150 | + }, |
| 151 | + Tags: []string{"npm", "install", "not_found"}, |
| 152 | + }) |
| 153 | + |
| 154 | + samples = append(samples, BenchmarkSample{ |
| 155 | + ID: "npm-002", |
| 156 | + Command: "npm install typescript --save-dev", |
| 157 | + ExitCode: 1, |
| 158 | + Stderr: "npm ERR! code ERESOLVE\nnpm ERR! ERESOLVE unable to resolve dependency tree\nnpm ERR! peer dep missing: react@^18.0.0", |
| 159 | + Context: SmartFallbackContext{ |
| 160 | + CommandType: "nodejs", |
| 161 | + Command: "npm", |
| 162 | + Subcommand: "install", |
| 163 | + ProjectType: "node", |
| 164 | + ErrorPattern: "dependency", |
| 165 | + }, |
| 166 | + Category: "npm", |
| 167 | + Description: "Dependency resolution failure", |
| 168 | + GoldInsults: []string{ |
| 169 | + "Dependency hell. You're everyone's least favorite dependency.", |
| 170 | + "ERESOLVE: Can't resolve your incompetence either", |
| 171 | + }, |
| 172 | + Tags: []string{"npm", "install", "dependency"}, |
| 173 | + }) |
| 174 | + |
| 175 | + samples = append(samples, BenchmarkSample{ |
| 176 | + ID: "npm-003", |
| 177 | + Command: "npm test", |
| 178 | + ExitCode: 1, |
| 179 | + Stderr: "FAIL src/components/App.test.js\n ● App › renders correctly\n expect(received).toEqual(expected)\n Expected: true\n Received: false", |
| 180 | + Context: SmartFallbackContext{ |
| 181 | + CommandType: "nodejs", |
| 182 | + Command: "npm", |
| 183 | + Subcommand: "test", |
| 184 | + ProjectType: "node", |
| 185 | + ErrorPattern: "test_failure", |
| 186 | + IsCI: true, |
| 187 | + CIProvider: "github", |
| 188 | + }, |
| 189 | + Category: "npm", |
| 190 | + Description: "Test failure in CI", |
| 191 | + GoldInsults: []string{ |
| 192 | + "Tests failed. Shocking absolutely no one who read your code", |
| 193 | + "Did you test this before committing? Oh wait, that's what CI is for", |
| 194 | + }, |
| 195 | + Tags: []string{"npm", "test", "test_failure", "ci"}, |
| 196 | + }) |
| 197 | + |
| 198 | + // Docker failures |
| 199 | + samples = append(samples, BenchmarkSample{ |
| 200 | + ID: "docker-001", |
| 201 | + Command: "docker build -t myapp .", |
| 202 | + ExitCode: 1, |
| 203 | + Stderr: "Step 5/10 : RUN npm install\nERROR [5/10] RUN npm install\nfailed to solve with frontend dockerfile.v0", |
| 204 | + Context: SmartFallbackContext{ |
| 205 | + CommandType: "docker", |
| 206 | + Command: "docker", |
| 207 | + Subcommand: "build", |
| 208 | + HasDockerfile: true, |
| 209 | + ErrorPattern: "build_failure", |
| 210 | + }, |
| 211 | + Category: "docker", |
| 212 | + Description: "Docker build failure", |
| 213 | + GoldInsults: []string{ |
| 214 | + "Docker build failed. Can't containerize disaster.", |
| 215 | + "FROM scratch. You are scratch.", |
| 216 | + }, |
| 217 | + Tags: []string{"docker", "build", "build_failure"}, |
| 218 | + }) |
| 219 | + |
| 220 | + samples = append(samples, BenchmarkSample{ |
| 221 | + ID: "docker-002", |
| 222 | + Command: "docker run -p 3000:3000 myapp", |
| 223 | + ExitCode: 125, |
| 224 | + Stderr: "docker: Error response from daemon: driver failed programming external connectivity on endpoint\nError starting userland proxy: listen tcp4 0.0.0.0:3000: bind: address already in use.", |
| 225 | + Context: SmartFallbackContext{ |
| 226 | + CommandType: "docker", |
| 227 | + Command: "docker", |
| 228 | + Subcommand: "run", |
| 229 | + ErrorPattern: "port_in_use", |
| 230 | + NumericArgs: []int{3000}, |
| 231 | + }, |
| 232 | + Category: "docker", |
| 233 | + Description: "Port already in use", |
| 234 | + GoldInsults: []string{ |
| 235 | + "Port 3000 already in use. By someone competent, probably.", |
| 236 | + "Port conflict. Your existence is a conflict.", |
| 237 | + }, |
| 238 | + Tags: []string{"docker", "run", "network"}, |
| 239 | + }) |
| 240 | + |
| 241 | + // Python failures |
| 242 | + samples = append(samples, BenchmarkSample{ |
| 243 | + ID: "python-001", |
| 244 | + Command: "python app.py", |
| 245 | + ExitCode: 1, |
| 246 | + Stderr: "Traceback (most recent call last):\n File \"app.py\", line 5, in <module>\n import requests\nModuleNotFoundError: No module named 'requests'", |
| 247 | + Context: SmartFallbackContext{ |
| 248 | + CommandType: "python", |
| 249 | + Command: "python", |
| 250 | + ProjectType: "python", |
| 251 | + ErrorPattern: "dependency", |
| 252 | + FileExtensions: []string{".py"}, |
| 253 | + }, |
| 254 | + Category: "python", |
| 255 | + Description: "Missing Python module", |
| 256 | + GoldInsults: []string{ |
| 257 | + "ModuleNotFoundError: Module 'brain' not found", |
| 258 | + "Did you activate your venv? Don't answer, I know you didn't", |
| 259 | + }, |
| 260 | + Tags: []string{"python", "dependency"}, |
| 261 | + }) |
| 262 | + |
| 263 | + samples = append(samples, BenchmarkSample{ |
| 264 | + ID: "python-002", |
| 265 | + Command: "python script.py", |
| 266 | + ExitCode: 1, |
| 267 | + Stderr: " File \"script.py\", line 15\n if x == 5\nSyntaxError: invalid syntax", |
| 268 | + Context: SmartFallbackContext{ |
| 269 | + CommandType: "python", |
| 270 | + Command: "python", |
| 271 | + ProjectType: "python", |
| 272 | + ErrorPattern: "syntax_error", |
| 273 | + FileExtensions: []string{".py"}, |
| 274 | + }, |
| 275 | + Category: "python", |
| 276 | + Description: "Python syntax error", |
| 277 | + GoldInsults: []string{ |
| 278 | + "SyntaxError: Invalid syntax, invalid developer", |
| 279 | + "Python is trying to tell you something. Maybe listen for once?", |
| 280 | + }, |
| 281 | + Tags: []string{"python", "syntax"}, |
| 282 | + }) |
| 283 | + |
| 284 | + // Rust failures |
| 285 | + samples = append(samples, BenchmarkSample{ |
| 286 | + ID: "rust-001", |
| 287 | + Command: "cargo build", |
| 288 | + ExitCode: 101, |
| 289 | + Stderr: "error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable\n --> src/main.rs:10:5", |
| 290 | + Context: SmartFallbackContext{ |
| 291 | + CommandType: "rust", |
| 292 | + Command: "cargo", |
| 293 | + Subcommand: "build", |
| 294 | + ProjectType: "rust", |
| 295 | + ErrorPattern: "borrow_checker", |
| 296 | + }, |
| 297 | + Category: "rust", |
| 298 | + Description: "Borrow checker error", |
| 299 | + GoldInsults: []string{ |
| 300 | + "Borrow checker says no. And honestly, it has a point.", |
| 301 | + "Fighting the borrow checker? The borrow checker always wins.", |
| 302 | + }, |
| 303 | + Tags: []string{"rust", "build", "borrow_checker"}, |
| 304 | + }) |
| 305 | + |
| 306 | + // Permission errors |
| 307 | + samples = append(samples, BenchmarkSample{ |
| 308 | + ID: "perm-001", |
| 309 | + Command: "chmod 777 /etc/passwd", |
| 310 | + ExitCode: 1, |
| 311 | + Stderr: "chmod: changing permissions of '/etc/passwd': Operation not permitted", |
| 312 | + Context: SmartFallbackContext{ |
| 313 | + Command: "chmod", |
| 314 | + ErrorPattern: "permission_denied", |
| 315 | + NumericArgs: []int{777}, |
| 316 | + }, |
| 317 | + Category: "permission", |
| 318 | + Description: "Permission denied with chmod 777", |
| 319 | + GoldInsults: []string{ |
| 320 | + "chmod 777 isn't the answer this time, though I admire your optimism", |
| 321 | + "777: Jackpot of incompetence", |
| 322 | + }, |
| 323 | + Tags: []string{"permission", "chmod"}, |
| 324 | + }) |
| 325 | + |
| 326 | + // Late night scenarios |
| 327 | + samples = append(samples, BenchmarkSample{ |
| 328 | + ID: "time-001", |
| 329 | + Command: "make build", |
| 330 | + ExitCode: 2, |
| 331 | + Stderr: "make: *** [Makefile:15: build] Error 2", |
| 332 | + Context: SmartFallbackContext{ |
| 333 | + Command: "make", |
| 334 | + ErrorPattern: "build_failure", |
| 335 | + TimeOfDay: 3, |
| 336 | + HasMakefile: true, |
| 337 | + }, |
| 338 | + Category: "build", |
| 339 | + Description: "Build failure at 3 AM", |
| 340 | + GoldInsults: []string{ |
| 341 | + "It's 3 AM. The bugs aren't the only thing that needs fixing", |
| 342 | + "Late night debugging? Tomorrow-you is going to hate today-you", |
| 343 | + }, |
| 344 | + Tags: []string{"build", "late_night"}, |
| 345 | + }) |
| 346 | + |
| 347 | + return samples |
| 348 | +} |
| 349 | + |
| 350 | +// EvaluateSystem runs the benchmark against a system |
| 351 | +func (b *Benchmark) EvaluateSystem(system *EnsembleSystem) BenchmarkResults { |
| 352 | + results := BenchmarkResults{ |
| 353 | + SystemName: "Ensemble ML System", |
| 354 | + TotalSamples: len(b.Samples), |
| 355 | + DetailedScores: make([]SampleScore, 0, len(b.Samples)), |
| 356 | + } |
| 357 | + |
| 358 | + var totalRelevance float64 |
| 359 | + var totalLatency time.Duration |
| 360 | + var totalConfidence float64 |
| 361 | + var fallbackCount int |
| 362 | + |
| 363 | + for _, sample := range b.Samples { |
| 364 | + start := time.Now() |
| 365 | + insult := system.GenerateInsult(&sample.Context, "sarcastic") |
| 366 | + latency := time.Since(start) |
| 367 | + |
| 368 | + // Calculate relevance score |
| 369 | + relevance := calculateRelevanceScore(sample, insult) |
| 370 | + |
| 371 | + // Determine if it was a Markov fallback |
| 372 | + isFallback := len(insult) > 0 && !containsInsult(system.database.Insults, insult) |
| 373 | + |
| 374 | + if isFallback { |
| 375 | + fallbackCount++ |
| 376 | + } |
| 377 | + |
| 378 | + score := SampleScore{ |
| 379 | + SampleID: sample.ID, |
| 380 | + GeneratedInsult: insult, |
| 381 | + Relevance: relevance, |
| 382 | + Latency: latency, |
| 383 | + Confidence: 0.75, // Placeholder |
| 384 | + NoveltyScore: 1.0, |
| 385 | + Method: determineMethod(isFallback), |
| 386 | + } |
| 387 | + |
| 388 | + results.DetailedScores = append(results.DetailedScores, score) |
| 389 | + |
| 390 | + totalRelevance += relevance |
| 391 | + totalLatency += latency |
| 392 | + totalConfidence += score.Confidence |
| 393 | + } |
| 394 | + |
| 395 | + results.AvgRelevance = totalRelevance / float64(len(b.Samples)) |
| 396 | + results.AvgLatency = totalLatency / time.Duration(len(b.Samples)) |
| 397 | + results.AvgConfidence = totalConfidence / float64(len(b.Samples)) |
| 398 | + results.FallbackRate = float64(fallbackCount) / float64(len(b.Samples)) |
| 399 | + results.DiversityScore = calculateDiversityScore(results.DetailedScores) |
| 400 | + |
| 401 | + return results |
| 402 | +} |
| 403 | + |
| 404 | +// calculateRelevanceScore measures how relevant the insult is to the error |
| 405 | +func calculateRelevanceScore(sample BenchmarkSample, insult string) float64 { |
| 406 | + score := 0.0 |
| 407 | + |
| 408 | + // Check for keyword matches |
| 409 | + keywords := extractKeywords(sample) |
| 410 | + for _, keyword := range keywords { |
| 411 | + if containsWord(insult, keyword) { |
| 412 | + score += 0.2 |
| 413 | + } |
| 414 | + } |
| 415 | + |
| 416 | + // Check for tag matches |
| 417 | + for _, tag := range sample.Tags { |
| 418 | + if containsWord(insult, tag) { |
| 419 | + score += 0.15 |
| 420 | + } |
| 421 | + } |
| 422 | + |
| 423 | + // Check similarity to gold insults |
| 424 | + if len(sample.GoldInsults) > 0 { |
| 425 | + maxSimilarity := 0.0 |
| 426 | + for _, gold := range sample.GoldInsults { |
| 427 | + sim := simpleStringSimilarity(insult, gold) |
| 428 | + if sim > maxSimilarity { |
| 429 | + maxSimilarity = sim |
| 430 | + } |
| 431 | + } |
| 432 | + score += maxSimilarity * 0.3 |
| 433 | + } |
| 434 | + |
| 435 | + return math.Min(1.0, score) |
| 436 | +} |
| 437 | + |
| 438 | +// extractKeywords extracts key terms from sample |
| 439 | +func extractKeywords(sample BenchmarkSample) []string { |
| 440 | + keywords := []string{ |
| 441 | + sample.Context.Command, |
| 442 | + sample.Context.Subcommand, |
| 443 | + sample.Context.CommandType, |
| 444 | + sample.Context.ErrorPattern, |
| 445 | + } |
| 446 | + |
| 447 | + if sample.Context.GitBranch != "" { |
| 448 | + keywords = append(keywords, sample.Context.GitBranch) |
| 449 | + } |
| 450 | + |
| 451 | + if sample.Context.ProjectType != "" { |
| 452 | + keywords = append(keywords, sample.Context.ProjectType) |
| 453 | + } |
| 454 | + |
| 455 | + return keywords |
| 456 | +} |
| 457 | + |
| 458 | +// containsWord checks if text contains word (case-insensitive) |
| 459 | +func containsWord(text, word string) bool { |
| 460 | + textLower := toLower(text) |
| 461 | + wordLower := toLower(word) |
| 462 | + return contains(textLower, wordLower) |
| 463 | +} |
| 464 | + |
| 465 | +// simpleStringSimilarity calculates basic string similarity |
| 466 | +func simpleStringSimilarity(s1, s2 string) float64 { |
| 467 | + // Simple word overlap metric |
| 468 | + words1 := splitWords(toLower(s1)) |
| 469 | + words2 := splitWords(toLower(s2)) |
| 470 | + |
| 471 | + if len(words1) == 0 || len(words2) == 0 { |
| 472 | + return 0.0 |
| 473 | + } |
| 474 | + |
| 475 | + matches := 0 |
| 476 | + for _, w1 := range words1 { |
| 477 | + for _, w2 := range words2 { |
| 478 | + if w1 == w2 && len(w1) > 2 { // Skip short words |
| 479 | + matches++ |
| 480 | + break |
| 481 | + } |
| 482 | + } |
| 483 | + } |
| 484 | + |
| 485 | + return float64(matches) / float64(max(len(words1), len(words2))) |
| 486 | +} |
| 487 | + |
| 488 | +// calculateDiversityScore measures insult variety |
| 489 | +func calculateDiversityScore(scores []SampleScore) float64 { |
| 490 | + if len(scores) < 2 { |
| 491 | + return 1.0 |
| 492 | + } |
| 493 | + |
| 494 | + // Count unique insults |
| 495 | + unique := make(map[string]bool) |
| 496 | + for _, score := range scores { |
| 497 | + unique[score.GeneratedInsult] = true |
| 498 | + } |
| 499 | + |
| 500 | + return float64(len(unique)) / float64(len(scores)) |
| 501 | +} |
| 502 | + |
| 503 | +// containsInsult checks if insult exists in database |
| 504 | +func containsInsult(insults []TaggedInsult, target string) bool { |
| 505 | + for _, insult := range insults { |
| 506 | + if insult.Text == target { |
| 507 | + return true |
| 508 | + } |
| 509 | + } |
| 510 | + return false |
| 511 | +} |
| 512 | + |
| 513 | +// determineMethod identifies which method generated the insult |
| 514 | +func determineMethod(isFallback bool) string { |
| 515 | + if isFallback { |
| 516 | + return "markov" |
| 517 | + } |
| 518 | + return "ensemble" |
| 519 | +} |
| 520 | + |
| 521 | +// PrintResults outputs benchmark results |
| 522 | +func (r *BenchmarkResults) Print() { |
| 523 | + fmt.Println("╔═══════════════════════════════════════════════════════════╗") |
| 524 | + fmt.Printf("║ Benchmark Results: %-38s ║\n", r.SystemName) |
| 525 | + fmt.Println("╠═══════════════════════════════════════════════════════════╣") |
| 526 | + fmt.Printf("║ Total Samples: %-41d ║\n", r.TotalSamples) |
| 527 | + fmt.Printf("║ Avg Relevance: %-41.3f ║\n", r.AvgRelevance) |
| 528 | + fmt.Printf("║ Avg Latency: %-41s ║\n", r.AvgLatency) |
| 529 | + fmt.Printf("║ Avg Confidence: %-41.3f ║\n", r.AvgConfidence) |
| 530 | + fmt.Printf("║ Diversity Score: %-41.3f ║\n", r.DiversityScore) |
| 531 | + fmt.Printf("║ Fallback Rate: %-40.1f%% ║\n", r.FallbackRate*100) |
| 532 | + fmt.Println("╚═══════════════════════════════════════════════════════════╝") |
| 533 | +} |
| 534 | + |
| 535 | +// Helper functions |
| 536 | +func toLower(s string) string { |
| 537 | + result := "" |
| 538 | + for _, r := range s { |
| 539 | + if r >= 'A' && r <= 'Z' { |
| 540 | + result += string(r + 32) |
| 541 | + } else { |
| 542 | + result += string(r) |
| 543 | + } |
| 544 | + } |
| 545 | + return result |
| 546 | +} |
| 547 | + |
| 548 | +func contains(s, substr string) bool { |
| 549 | + return len(s) >= len(substr) && findSubstring(s, substr) >= 0 |
| 550 | +} |
| 551 | + |
| 552 | +func findSubstring(s, substr string) int { |
| 553 | + for i := 0; i <= len(s)-len(substr); i++ { |
| 554 | + if s[i:i+len(substr)] == substr { |
| 555 | + return i |
| 556 | + } |
| 557 | + } |
| 558 | + return -1 |
| 559 | +} |
| 560 | + |
| 561 | +func splitWords(s string) []string { |
| 562 | + var words []string |
| 563 | + var current string |
| 564 | + |
| 565 | + for _, r := range s { |
| 566 | + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') { |
| 567 | + current += string(r) |
| 568 | + } else { |
| 569 | + if len(current) > 0 { |
| 570 | + words = append(words, current) |
| 571 | + current = "" |
| 572 | + } |
| 573 | + } |
| 574 | + } |
| 575 | + |
| 576 | + if len(current) > 0 { |
| 577 | + words = append(words, current) |
| 578 | + } |
| 579 | + |
| 580 | + return words |
| 581 | +} |
| 582 | + |
| 583 | +func max(a, b int) int { |
| 584 | + if a > b { |
| 585 | + return a |
| 586 | + } |
| 587 | + return b |
| 588 | +} |