TitleAnalysisEngine.swift 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import Foundation
  2. enum TitleAnalysisEngine {
  3. static func analyze(title: String, goal: TitleGoal) -> TitleAnalysis {
  4. let length = title.count
  5. let lengthScore = scoreLength(length)
  6. let engagementScore = scoreEngagement(title, goal: goal)
  7. let clarityScore = scoreClarity(title)
  8. let overall = (lengthScore + engagementScore + clarityScore) / 3
  9. let suggestions = makeSuggestions(title: title, length: length, goal: goal)
  10. return TitleAnalysis(
  11. overallScore: overall,
  12. lengthScore: lengthScore,
  13. engagementScore: engagementScore,
  14. clarityScore: clarityScore,
  15. suggestions: suggestions
  16. )
  17. }
  18. private static func scoreLength(_ length: Int) -> Int {
  19. switch length {
  20. case 0: 0
  21. case 1...50: 95
  22. case 51...80: 85
  23. case 81...120: 70
  24. case 121...200: 50
  25. case 201...300: 30
  26. default: 10
  27. }
  28. }
  29. private static func scoreEngagement(_ title: String, goal: TitleGoal) -> Int {
  30. var score = 50
  31. let lower = title.lowercased()
  32. if title.contains("?") { score += 10 }
  33. if title.range(of: #"\d+"#, options: .regularExpression) != nil { score += 8 }
  34. if lower.contains("how") || lower.contains("why") || lower.contains("what") { score += 6 }
  35. if lower.contains("you") || lower.contains("your") { score += 5 }
  36. if title == title.uppercased() && title.count > 5 { score -= 15 }
  37. switch goal {
  38. case .question where !title.contains("?"): score -= 10
  39. case .listicle where title.range(of: #"\d+"#, options: .regularExpression) == nil: score -= 8
  40. case .curiosity where !lower.contains("secret") && !lower.contains("nobody") && !lower.contains("actually"): score -= 3
  41. default: break
  42. }
  43. return min(100, max(0, score))
  44. }
  45. private static func scoreClarity(_ title: String) -> Int {
  46. var score = 70
  47. let words = title.split(separator: " ").count
  48. if words >= 4 && words <= 12 { score += 15 }
  49. if words < 3 { score -= 20 }
  50. if words > 20 { score -= 15 }
  51. if title.contains(" ") { score -= 5 }
  52. if title.hasPrefix(" ") || title.hasSuffix(" ") { score -= 10 }
  53. return min(100, max(0, score))
  54. }
  55. private static func makeSuggestions(title: String, length: Int, goal: TitleGoal) -> [String] {
  56. var tips: [String] = []
  57. if length > 120 {
  58. tips.append("Shorten to under 80 characters for better mobile visibility.")
  59. } else if length < 20 {
  60. tips.append("Add more context — very short titles can feel vague in feeds.")
  61. }
  62. if !title.contains("?") && goal == .question {
  63. tips.append("Reframe as a question to invite comments.")
  64. }
  65. if title.range(of: #"\d+"#, options: .regularExpression) == nil && goal == .listicle {
  66. tips.append("Include a number (e.g. \"5 tips\") for listicle-style engagement.")
  67. }
  68. let lower = title.lowercased()
  69. if goal == .curiosity && !lower.contains("?") {
  70. tips.append("Add a curiosity hook — hint at a surprising outcome.")
  71. }
  72. if tips.isEmpty {
  73. tips.append("Strong base title — try variants with different hooks below.")
  74. }
  75. return tips
  76. }
  77. }