@@ -0,0 +1,538 @@ |
| 1 | +package llm |
| 2 | + |
| 3 | +import ( |
| 4 | + "math" |
| 5 | + "math/rand" |
| 6 | + "strings" |
| 7 | + "time" |
| 8 | +) |
| 9 | + |
| 10 | +// AdversarialInsultGenerator implements a GAN-inspired system |
| 11 | +// Generator creates insults, Critic scores them, iterative improvement |
| 12 | +type AdversarialInsultGenerator struct { |
| 13 | + generator *InsultGenerator |
| 14 | + critic *InsultCritic |
| 15 | + database *InsultDatabase |
| 16 | + markov *MarkovGenerator |
| 17 | + |
| 18 | + // Training state |
| 19 | + generatorScore float64 |
| 20 | + criticScore float64 |
| 21 | + rounds int |
| 22 | + |
| 23 | + // Quality thresholds |
| 24 | + minQuality float64 |
| 25 | + targetQuality float64 |
| 26 | + improvementRate float64 |
| 27 | +} |
| 28 | + |
| 29 | +// InsultGenerator creates insult candidates |
| 30 | +type InsultGenerator struct { |
| 31 | + templates []string |
| 32 | + components *ComponentLibrary |
| 33 | + markov *MarkovGenerator |
| 34 | + creativityMode string // "safe", "balanced", "wild" |
| 35 | + rng *rand.Rand |
| 36 | +} |
| 37 | + |
| 38 | +// InsultCritic scores insult quality |
| 39 | +type InsultCritic struct { |
| 40 | + relevanceWeight float64 |
| 41 | + noveltyWeight float64 |
| 42 | + brutalityWeight float64 |
| 43 | + coherenceWeight float64 |
| 44 | + lengthWeight float64 |
| 45 | + |
| 46 | + seenInsults map[string]int // Track seen insults for novelty |
| 47 | +} |
| 48 | + |
| 49 | +// ComponentLibrary stores semantic building blocks |
| 50 | +type ComponentLibrary struct { |
| 51 | + Subjects []string // "Your code", "This commit", "Your docker build" |
| 52 | + Verbs []string // "failed harder than", "crashed like", "died faster than" |
| 53 | + Objects []string // "your career", "a burning dumpster", "your hopes" |
| 54 | + Punchlines []string // "combined", "on steroids", "in production" |
| 55 | + Intensifiers []string // "absolutely", "completely", "monumentally" |
| 56 | + Comparisons []string // "than a", "like a", "as much as" |
| 57 | +} |
| 58 | + |
| 59 | +// InsultQualityScore represents multi-dimensional quality metrics |
| 60 | +type InsultQualityScore struct { |
| 61 | + Relevance float64 // How well it matches the context |
| 62 | + Novelty float64 // How original/unique it is |
| 63 | + Brutality float64 // How savage/brutal it is |
| 64 | + Coherence float64 // How well it flows/makes sense |
| 65 | + Length float64 // Appropriate length (not too long/short) |
| 66 | + Overall float64 // Weighted combination |
| 67 | + Breakdown string // Human-readable explanation |
| 68 | +} |
| 69 | + |
| 70 | +// NewAdversarialInsultGenerator creates a GAN-inspired generator |
| 71 | +func NewAdversarialInsultGenerator(db *InsultDatabase, markov *MarkovGenerator) *AdversarialInsultGenerator { |
| 72 | + components := initializeComponentLibrary() |
| 73 | + |
| 74 | + return &AdversarialInsultGenerator{ |
| 75 | + generator: &InsultGenerator{ |
| 76 | + templates: initializeTemplates(), |
| 77 | + components: components, |
| 78 | + markov: markov, |
| 79 | + creativityMode: "balanced", |
| 80 | + rng: rand.New(rand.NewSource(time.Now().UnixNano())), |
| 81 | + }, |
| 82 | + critic: &InsultCritic{ |
| 83 | + relevanceWeight: 0.30, |
| 84 | + noveltyWeight: 0.25, |
| 85 | + brutalityWeight: 0.20, |
| 86 | + coherenceWeight: 0.15, |
| 87 | + lengthWeight: 0.10, |
| 88 | + seenInsults: make(map[string]int), |
| 89 | + }, |
| 90 | + database: db, |
| 91 | + markov: markov, |
| 92 | + generatorScore: 0.5, |
| 93 | + criticScore: 0.5, |
| 94 | + rounds: 0, |
| 95 | + minQuality: 0.6, |
| 96 | + targetQuality: 0.8, |
| 97 | + improvementRate: 0.05, |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +// Generate creates an insult using adversarial training |
| 102 | +func (aig *AdversarialInsultGenerator) Generate(ctx *SmartFallbackContext, personality string) string { |
| 103 | + const maxAttempts = 5 |
| 104 | + bestInsult := "" |
| 105 | + bestScore := 0.0 |
| 106 | + |
| 107 | + // Generate multiple candidates and pick the best |
| 108 | + for attempt := 0; attempt < maxAttempts; attempt++ { |
| 109 | + // Generator creates candidate |
| 110 | + candidate := aig.generator.CreateCandidate(ctx, personality) |
| 111 | + |
| 112 | + // Critic scores it |
| 113 | + score := aig.critic.Score(candidate, ctx, personality) |
| 114 | + |
| 115 | + // Track best |
| 116 | + if score.Overall > bestScore { |
| 117 | + bestScore = score.Overall |
| 118 | + bestInsult = candidate |
| 119 | + } |
| 120 | + |
| 121 | + // If we hit target quality, stop early |
| 122 | + if score.Overall >= aig.targetQuality { |
| 123 | + break |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + // Record this insult for novelty tracking |
| 128 | + aig.critic.seenInsults[bestInsult]++ |
| 129 | + |
| 130 | + // Update adversarial scores (simplified GAN-like training) |
| 131 | + aig.updateScores(bestScore) |
| 132 | + |
| 133 | + // Only return if above minimum quality |
| 134 | + if bestScore >= aig.minQuality { |
| 135 | + return bestInsult |
| 136 | + } |
| 137 | + |
| 138 | + return "" // Not good enough |
| 139 | +} |
| 140 | + |
| 141 | +// GenerateBatch creates multiple candidates for ensemble selection |
| 142 | +func (aig *AdversarialInsultGenerator) GenerateBatch(ctx *SmartFallbackContext, personality string, count int) []string { |
| 143 | + candidates := make([]string, 0, count) |
| 144 | + |
| 145 | + for i := 0; i < count; i++ { |
| 146 | + candidate := aig.generator.CreateCandidate(ctx, personality) |
| 147 | + score := aig.critic.Score(candidate, ctx, personality) |
| 148 | + |
| 149 | + if score.Overall >= aig.minQuality { |
| 150 | + candidates = append(candidates, candidate) |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + return candidates |
| 155 | +} |
| 156 | + |
| 157 | +// CreateCandidate generates an insult candidate |
| 158 | +func (ig *InsultGenerator) CreateCandidate(ctx *SmartFallbackContext, personality string) string { |
| 159 | + // Choose generation strategy based on creativity mode |
| 160 | + strategy := ig.rng.Float64() |
| 161 | + |
| 162 | + switch ig.creativityMode { |
| 163 | + case "safe": |
| 164 | + // Mostly template-based (70%), some Markov (30%) |
| 165 | + if strategy < 0.7 { |
| 166 | + return ig.generateFromTemplate(ctx, personality) |
| 167 | + } |
| 168 | + return ig.markov.Blend(ctx) |
| 169 | + |
| 170 | + case "wild": |
| 171 | + // Mostly Markov (60%), some composites (30%), some templates (10%) |
| 172 | + if strategy < 0.6 { |
| 173 | + return ig.markov.Blend(ctx) |
| 174 | + } else if strategy < 0.9 { |
| 175 | + return ig.generateComposite(ctx, personality) |
| 176 | + } |
| 177 | + return ig.generateFromTemplate(ctx, personality) |
| 178 | + |
| 179 | + default: // "balanced" |
| 180 | + // Mix of all: 40% template, 40% composite, 20% Markov |
| 181 | + if strategy < 0.4 { |
| 182 | + return ig.generateFromTemplate(ctx, personality) |
| 183 | + } else if strategy < 0.8 { |
| 184 | + return ig.generateComposite(ctx, personality) |
| 185 | + } |
| 186 | + return ig.markov.Blend(ctx) |
| 187 | + } |
| 188 | +} |
| 189 | + |
| 190 | +// generateFromTemplate uses traditional templates |
| 191 | +func (ig *InsultGenerator) generateFromTemplate(ctx *SmartFallbackContext, personality string) string { |
| 192 | + template := ig.templates[ig.rng.Intn(len(ig.templates))] |
| 193 | + |
| 194 | + // Replace variables |
| 195 | + result := template |
| 196 | + result = strings.ReplaceAll(result, "{command}", ctx.Command) |
| 197 | + result = strings.ReplaceAll(result, "{commandType}", ctx.CommandType) |
| 198 | + result = strings.ReplaceAll(result, "{error}", ctx.ErrorPattern) |
| 199 | + result = strings.ReplaceAll(result, "{project}", ctx.ProjectType) |
| 200 | + |
| 201 | + return result |
| 202 | +} |
| 203 | + |
| 204 | +// generateComposite builds from semantic components |
| 205 | +func (ig *InsultGenerator) generateComposite(ctx *SmartFallbackContext, personality string) string { |
| 206 | + comp := ig.components |
| 207 | + |
| 208 | + // Build: [Subject] [Verb] [Object] [Punchline] |
| 209 | + subject := comp.Subjects[ig.rng.Intn(len(comp.Subjects))] |
| 210 | + verb := comp.Verbs[ig.rng.Intn(len(comp.Verbs))] |
| 211 | + object := comp.Objects[ig.rng.Intn(len(comp.Objects))] |
| 212 | + |
| 213 | + // Contextualize subject |
| 214 | + subject = ig.contextualizeSubject(subject, ctx) |
| 215 | + |
| 216 | + // Sometimes add intensifier |
| 217 | + insult := subject + " " + verb + " " + object |
| 218 | + |
| 219 | + // Sometimes add punchline |
| 220 | + if ig.rng.Float64() < 0.5 { |
| 221 | + punchline := comp.Punchlines[ig.rng.Intn(len(comp.Punchlines))] |
| 222 | + insult += " " + punchline |
| 223 | + } |
| 224 | + |
| 225 | + // Ensure it ends with punctuation |
| 226 | + if !strings.HasSuffix(insult, ".") && !strings.HasSuffix(insult, "!") { |
| 227 | + insult += "." |
| 228 | + } |
| 229 | + |
| 230 | + return insult |
| 231 | +} |
| 232 | + |
| 233 | +// contextualizeSubject adapts subject to context |
| 234 | +func (ig *InsultGenerator) contextualizeSubject(subject string, ctx *SmartFallbackContext) string { |
| 235 | + // Replace generic "Your code" with specific command references |
| 236 | + if ctx.Command != "" { |
| 237 | + subject = strings.ReplaceAll(subject, "Your code", "Your "+ctx.Command) |
| 238 | + subject = strings.ReplaceAll(subject, "This code", "This "+ctx.Command) |
| 239 | + } |
| 240 | + |
| 241 | + if ctx.ProjectType != "" { |
| 242 | + subject = strings.ReplaceAll(subject, "project", ctx.ProjectType+" project") |
| 243 | + } |
| 244 | + |
| 245 | + return subject |
| 246 | +} |
| 247 | + |
| 248 | +// Score evaluates insult quality |
| 249 | +func (ic *InsultCritic) Score(insult string, ctx *SmartFallbackContext, personality string) InsultQualityScore { |
| 250 | + score := InsultQualityScore{} |
| 251 | + |
| 252 | + // 1. Relevance: Does it relate to the context? |
| 253 | + score.Relevance = ic.scoreRelevance(insult, ctx) |
| 254 | + |
| 255 | + // 2. Novelty: Is it unique/original? |
| 256 | + score.Novelty = ic.scoreNovelty(insult) |
| 257 | + |
| 258 | + // 3. Brutality: How savage is it? (adjust for personality) |
| 259 | + score.Brutality = ic.scoreBrutality(insult, personality) |
| 260 | + |
| 261 | + // 4. Coherence: Does it make sense? |
| 262 | + score.Coherence = ic.scoreCoherence(insult) |
| 263 | + |
| 264 | + // 5. Length: Is it appropriately sized? |
| 265 | + score.Length = ic.scoreLength(insult) |
| 266 | + |
| 267 | + // Calculate weighted overall score |
| 268 | + score.Overall = (score.Relevance * ic.relevanceWeight) + |
| 269 | + (score.Novelty * ic.noveltyWeight) + |
| 270 | + (score.Brutality * ic.brutalityWeight) + |
| 271 | + (score.Coherence * ic.coherenceWeight) + |
| 272 | + (score.Length * ic.lengthWeight) |
| 273 | + |
| 274 | + // Generate breakdown |
| 275 | + score.Breakdown = ic.generateBreakdown(score) |
| 276 | + |
| 277 | + return score |
| 278 | +} |
| 279 | + |
| 280 | +func (ic *InsultCritic) scoreRelevance(insult string, ctx *SmartFallbackContext) float64 { |
| 281 | + score := 0.0 |
| 282 | + insultLower := strings.ToLower(insult) |
| 283 | + |
| 284 | + // Check if it mentions the command |
| 285 | + if ctx.Command != "" && strings.Contains(insultLower, strings.ToLower(ctx.Command)) { |
| 286 | + score += 0.3 |
| 287 | + } |
| 288 | + |
| 289 | + // Check if it mentions the command type |
| 290 | + if ctx.CommandType != "" && strings.Contains(insultLower, strings.ToLower(ctx.CommandType)) { |
| 291 | + score += 0.2 |
| 292 | + } |
| 293 | + |
| 294 | + // Check if it mentions the error pattern |
| 295 | + if ctx.ErrorPattern != "" { |
| 296 | + errorWords := strings.Split(ctx.ErrorPattern, "_") |
| 297 | + for _, word := range errorWords { |
| 298 | + if strings.Contains(insultLower, strings.ToLower(word)) { |
| 299 | + score += 0.2 |
| 300 | + break |
| 301 | + } |
| 302 | + } |
| 303 | + } |
| 304 | + |
| 305 | + // Check if it mentions the project type |
| 306 | + if ctx.ProjectType != "" && strings.Contains(insultLower, strings.ToLower(ctx.ProjectType)) { |
| 307 | + score += 0.2 |
| 308 | + } |
| 309 | + |
| 310 | + // Bonus for context-aware terms |
| 311 | + contextTerms := []string{"failed", "error", "broken", "crash", "disaster"} |
| 312 | + for _, term := range contextTerms { |
| 313 | + if strings.Contains(insultLower, term) { |
| 314 | + score += 0.1 |
| 315 | + break |
| 316 | + } |
| 317 | + } |
| 318 | + |
| 319 | + return math.Min(1.0, score) |
| 320 | +} |
| 321 | + |
| 322 | +func (ic *InsultCritic) scoreNovelty(insult string) float64 { |
| 323 | + // Check how many times we've seen this exact insult |
| 324 | + useCount := ic.seenInsults[insult] |
| 325 | + |
| 326 | + // Novelty decays with use |
| 327 | + if useCount == 0 { |
| 328 | + return 1.0 // Completely novel |
| 329 | + } else if useCount == 1 { |
| 330 | + return 0.8 |
| 331 | + } else if useCount < 5 { |
| 332 | + return 0.6 |
| 333 | + } else if useCount < 10 { |
| 334 | + return 0.4 |
| 335 | + } |
| 336 | + return 0.2 // Heavily overused |
| 337 | +} |
| 338 | + |
| 339 | +func (ic *InsultCritic) scoreBrutality(insult string, personality string) float64 { |
| 340 | + insultLower := strings.ToLower(insult) |
| 341 | + |
| 342 | + // Count brutal words |
| 343 | + brutalWords := []string{ |
| 344 | + "disaster", "catastrophe", "failure", "incompetence", "terrible", |
| 345 | + "awful", "pathetic", "worthless", "garbage", "trash", "nightmare", |
| 346 | + "doomed", "hopeless", "broken", "destroyed", "ruined", "killed", |
| 347 | + } |
| 348 | + |
| 349 | + brutalCount := 0 |
| 350 | + for _, word := range brutalWords { |
| 351 | + if strings.Contains(insultLower, word) { |
| 352 | + brutalCount++ |
| 353 | + } |
| 354 | + } |
| 355 | + |
| 356 | + // Base brutality score |
| 357 | + brutality := math.Min(1.0, float64(brutalCount)*0.3) |
| 358 | + |
| 359 | + // Adjust for personality |
| 360 | + switch personality { |
| 361 | + case "mild": |
| 362 | + // Penalize high brutality |
| 363 | + if brutality > 0.5 { |
| 364 | + brutality *= 0.5 |
| 365 | + } |
| 366 | + case "savage": |
| 367 | + // Reward high brutality |
| 368 | + if brutality < 0.5 { |
| 369 | + brutality *= 0.7 |
| 370 | + } |
| 371 | + } |
| 372 | + |
| 373 | + return brutality |
| 374 | +} |
| 375 | + |
| 376 | +func (ic *InsultCritic) scoreCoherence(insult string) float64 { |
| 377 | + // Simple heuristics for coherence |
| 378 | + score := 1.0 |
| 379 | + |
| 380 | + // Penalize if too many numbers (likely corrupted) |
| 381 | + digitCount := 0 |
| 382 | + for _, r := range insult { |
| 383 | + if r >= '0' && r <= '9' { |
| 384 | + digitCount++ |
| 385 | + } |
| 386 | + } |
| 387 | + if digitCount > len(insult)/4 { |
| 388 | + score *= 0.5 |
| 389 | + } |
| 390 | + |
| 391 | + // Penalize if too many repeated words |
| 392 | + words := strings.Fields(insult) |
| 393 | + uniqueWords := make(map[string]bool) |
| 394 | + for _, word := range words { |
| 395 | + uniqueWords[strings.ToLower(word)] = true |
| 396 | + } |
| 397 | + if len(uniqueWords) < len(words)/2 { |
| 398 | + score *= 0.7 |
| 399 | + } |
| 400 | + |
| 401 | + // Penalize if no spaces (likely corrupted) |
| 402 | + if !strings.Contains(insult, " ") { |
| 403 | + score *= 0.3 |
| 404 | + } |
| 405 | + |
| 406 | + // Reward if it has proper punctuation |
| 407 | + if strings.HasSuffix(insult, ".") || strings.HasSuffix(insult, "!") { |
| 408 | + score *= 1.1 |
| 409 | + } |
| 410 | + |
| 411 | + return math.Min(1.0, score) |
| 412 | +} |
| 413 | + |
| 414 | +func (ic *InsultCritic) scoreLength(insult string) float64 { |
| 415 | + length := len(insult) |
| 416 | + |
| 417 | + // Optimal length: 40-120 characters |
| 418 | + if length >= 40 && length <= 120 { |
| 419 | + return 1.0 |
| 420 | + } else if length >= 20 && length < 40 { |
| 421 | + return 0.8 |
| 422 | + } else if length > 120 && length <= 150 { |
| 423 | + return 0.8 |
| 424 | + } else if length < 20 { |
| 425 | + return 0.5 // Too short |
| 426 | + } else { |
| 427 | + return 0.4 // Too long |
| 428 | + } |
| 429 | +} |
| 430 | + |
| 431 | +func (ic *InsultCritic) generateBreakdown(score InsultQualityScore) string { |
| 432 | + breakdown := "" |
| 433 | + breakdown += "Relevance: " + formatFloat(score.Relevance) + " " |
| 434 | + breakdown += "Novelty: " + formatFloat(score.Novelty) + " " |
| 435 | + breakdown += "Brutality: " + formatFloat(score.Brutality) + " " |
| 436 | + breakdown += "Coherence: " + formatFloat(score.Coherence) + " " |
| 437 | + breakdown += "Length: " + formatFloat(score.Length) |
| 438 | + return breakdown |
| 439 | +} |
| 440 | + |
| 441 | +// updateScores performs simplified GAN-like training |
| 442 | +func (aig *AdversarialInsultGenerator) updateScores(qualityScore float64) { |
| 443 | + // Generator score: how well it fooled the critic (inverse relationship) |
| 444 | + aig.generatorScore = 0.7*aig.generatorScore + 0.3*qualityScore |
| 445 | + |
| 446 | + // Critic score: how well it evaluated quality |
| 447 | + aig.criticScore = 0.7*aig.criticScore + 0.3*(1.0-math.Abs(qualityScore-0.8)) |
| 448 | + |
| 449 | + aig.rounds++ |
| 450 | + |
| 451 | + // Adjust creativity based on performance |
| 452 | + if aig.generatorScore < 0.5 && aig.rounds > 10 { |
| 453 | + // Generator struggling - be more conservative |
| 454 | + aig.generator.creativityMode = "safe" |
| 455 | + } else if aig.generatorScore > 0.7 { |
| 456 | + // Generator doing well - get more creative |
| 457 | + aig.generator.creativityMode = "wild" |
| 458 | + } else { |
| 459 | + aig.generator.creativityMode = "balanced" |
| 460 | + } |
| 461 | +} |
| 462 | + |
| 463 | +// GetStats returns adversarial system statistics |
| 464 | +func (aig *AdversarialInsultGenerator) GetStats() map[string]interface{} { |
| 465 | + return map[string]interface{}{ |
| 466 | + "generator_score": aig.generatorScore, |
| 467 | + "critic_score": aig.criticScore, |
| 468 | + "training_rounds": aig.rounds, |
| 469 | + "creativity_mode": aig.generator.creativityMode, |
| 470 | + "unique_insults": len(aig.critic.seenInsults), |
| 471 | + } |
| 472 | +} |
| 473 | + |
| 474 | +// Initialize component library |
| 475 | +func initializeComponentLibrary() *ComponentLibrary { |
| 476 | + return &ComponentLibrary{ |
| 477 | + Subjects: []string{ |
| 478 | + "Your code", "This commit", "Your build", "This deployment", |
| 479 | + "Your docker container", "This merge", "Your test suite", |
| 480 | + "Your pull request", "This branch", "Your configuration", |
| 481 | + "This error", "Your logic", "This attempt", "Your workflow", |
| 482 | + }, |
| 483 | + Verbs: []string{ |
| 484 | + "failed harder than", "crashed like", "died faster than", |
| 485 | + "broke worse than", "destroyed", "ruined", "devastated", |
| 486 | + "collapsed like", "imploded faster than", "crumbled like", |
| 487 | + "exploded worse than", "failed as badly as", "bombed harder than", |
| 488 | + }, |
| 489 | + Objects: []string{ |
| 490 | + "your career prospects", "a burning dumpster fire", "your hopes and dreams", |
| 491 | + "your last relationship", "the Hindenburg", "your job security", |
| 492 | + "a house of cards", "your professional reputation", "your confidence", |
| 493 | + "your self-esteem", "a Jenga tower", "your sanity", "reality itself", |
| 494 | + "everyone's expectations", "your manager's patience", |
| 495 | + }, |
| 496 | + Punchlines: []string{ |
| 497 | + "combined", "on steroids", "in production", "multiplied", |
| 498 | + "simultaneously", "at scale", "under load", "in the worst way", |
| 499 | + "and then some", "by orders of magnitude", "exponentially", |
| 500 | + }, |
| 501 | + Intensifiers: []string{ |
| 502 | + "absolutely", "completely", "utterly", "monumentally", |
| 503 | + "catastrophically", "spectacularly", "magnificently", |
| 504 | + }, |
| 505 | + Comparisons: []string{ |
| 506 | + "than", "like", "worse than", "as much as", "more than", |
| 507 | + }, |
| 508 | + } |
| 509 | +} |
| 510 | + |
| 511 | +// Initialize templates |
| 512 | +func initializeTemplates() []string { |
| 513 | + return []string{ |
| 514 | + "{command} failed: The {commandType} gods have rejected you.", |
| 515 | + "Your {command} encountered a {error}: Fix yourself first.", |
| 516 | + "{command} crashed harder than your career in {project}.", |
| 517 | + "Error in {command}: Expected competence, found disaster.", |
| 518 | + "Your {commandType} skills are as broken as this {command}.", |
| 519 | + "{command} failed. Your {project} project failed. You failed.", |
| 520 | + "The {error} error is just {command} protecting itself from you.", |
| 521 | + "Your {command} in {project} is a monument to failure.", |
| 522 | + } |
| 523 | +} |
| 524 | + |
| 525 | +func formatFloat(f float64) string { |
| 526 | + // Simple float formatter (0.00 format) |
| 527 | + intPart := int(f * 100) |
| 528 | + whole := intPart / 100 |
| 529 | + frac := intPart % 100 |
| 530 | + |
| 531 | + result := intToStr(whole) + "." |
| 532 | + if frac < 10 { |
| 533 | + result += "0" |
| 534 | + } |
| 535 | + result += intToStr(frac) |
| 536 | + |
| 537 | + return result |
| 538 | +} |