import Foundation /// Protocol for a calculation engine that evaluates text expressions. /// The primary implementation is RustCalculationEngine (FFI bridge to calcpad-engine). /// StubCalculationEngine is provided for testing without the Rust library. protocol CalculationEngine: Sendable { /// Evaluate a single line expression and return the result string, or nil for blank/comment lines. func evaluateLine(_ line: String) -> LineResult /// Evaluate an entire sheet (multi-line text) and return results for each line. func evaluateSheet(_ text: String) -> [LineResult] } /// Stub engine for testing that handles basic arithmetic (+, -, *, /). /// Used when the Rust engine library is not available. final class StubCalculationEngine: CalculationEngine { func evaluateLine(_ line: String) -> LineResult { evaluateSingleLine(line, lineNumber: 1) } func evaluateSheet(_ text: String) -> [LineResult] { let lines = text.components(separatedBy: "\n") return lines.enumerated().map { index, line in evaluateSingleLine(line, lineNumber: index + 1) } } private func evaluateSingleLine(_ line: String, lineNumber: Int) -> LineResult { let trimmed = line.trimmingCharacters(in: .whitespaces) // Blank lines produce no result guard !trimmed.isEmpty else { return LineResult(id: lineNumber, expression: line, result: nil, isError: false) } // Comment lines (starting with // or #) produce no result if trimmed.hasPrefix("//") || trimmed.hasPrefix("#") { return LineResult(id: lineNumber, expression: line, result: nil, isError: false) } // Convert integer literals to floating-point so division produces decimals. // NSExpression does integer division for "7 / 2" -> 3, but we want 3.5. let floatExpr = trimmed.replacingOccurrences( of: #"\b(\d+)\b"#, with: "$1.0", options: .regularExpression ) // Try to evaluate as a basic arithmetic expression do { let expr = NSExpression(format: floatExpr) if let value = expr.expressionValue(with: nil, context: nil) as? NSNumber { let doubleValue = value.doubleValue let formatted = String(format: "%g", doubleValue) return LineResult(id: lineNumber, expression: line, result: formatted, isError: false) } } return LineResult(id: lineNumber, expression: line, result: "Error", isError: true) } }