import Foundation public struct CalculatorResult: Sendable { public let displayValue: String public let copyValue: String? public let detail: String } public enum Calculator { private static let outputLocale = Locale(identifier: "en_US_POSIX") private static let functionNames: Set = [ "sqrt", "abs", "round", "floor", "ceil", "sin", "cos", "tan", "log", "ln", ] // ponytail: cap recursion for small debug worker stacks; use an iterative parser if deeper input is needed. private static let maximumDepth = 16 public static func evaluate(_ query: String) -> CalculatorResult? { guard query.utf8.count <= 512 else { return nil } let query = query.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { return nil } let words = query.split(whereSeparator: \.isWhitespace).map(String.init) if words.count >= 3 { for separator in 1..<(words.count - 1) where ["to", "in", "as"].contains(words[separator].lowercased()) { guard let target = ConversionUnit.parse(words[(separator + 1)...].joined(separator: " ")) else { continue } let sourceText = words[.. 1 else { continue } for length in stride(from: min(32, characters.count - 1), through: 1, by: -1) { guard let source = ConversionUnit.parse(String(characters.suffix(length))) else { continue } let expression = String(characters.dropLast(length)) guard let parsed = parse(expression) else { continue } let detail = "\(expression.trimmingCharacters(in: .whitespaces)) \(source.symbol) → \(target.symbol)" if let error = parsed.error { return failure(error, detail: detail) } guard source.family == target.family else { return failure("Incompatible units", detail: detail) } let value: Double if let sourceDimension = source.dimension, let targetDimension = target.dimension { value = Measurement(value: parsed.value, unit: sourceDimension) .converted(to: targetDimension).value } else { value = parsed.value * source.scale / target.scale } guard value.isFinite else { return failure("Result is not finite", detail: detail) } let text = "\(format(value)) \(target.symbol)" return CalculatorResult(displayValue: text, copyValue: text, detail: detail) } } } guard let parsed = parse(query), parsed.isExpression else { return nil } if let error = parsed.error { return failure(error, detail: query) } let text = format(parsed.value) return CalculatorResult(displayValue: text, copyValue: text, detail: query) } private static func failure(_ error: String, detail: String) -> CalculatorResult { CalculatorResult(displayValue: error, copyValue: nil, detail: detail) } private static func format(_ value: Double) -> String { String(format: "%.12g", locale: outputLocale, value == 0 ? 0 : value) } private static func parse(_ expression: String) -> Parser? { guard let tokens = tokenize(expression), !tokens.isEmpty else { return nil } var parser = Parser(tokens: tokens) do { parser.value = try parser.sum(depth: 0) guard parser.index == tokens.count else { return nil } return parser } catch { return nil } } private enum Token: Equatable { case number(Double), name(String) case plus, minus, multiply, divide, power, percent, modulo, of, off, left, right } private static func tokenize(_ expression: String) -> [Token]? { let characters = Array(expression) var tokens: [Token] = [] var index = 0 func digit(_ character: Character) -> Bool { character >= "0" && character <= "9" } while index < characters.count { let character = characters[index] if character.isWhitespace { index += 1; continue } if digit(character) || character == "." { let start = index var digits = 0 while index < characters.count, digit(characters[index]) { index += 1; digits += 1 } if index < characters.count, characters[index] == "." { index += 1 while index < characters.count, digit(characters[index]) { index += 1; digits += 1 } } guard digits > 0 else { return nil } if index < characters.count, characters[index] == "e" || characters[index] == "E" { index += 1 if index < characters.count, characters[index] == "+" || characters[index] == "-" { index += 1 } let exponentStart = index while index < characters.count, digit(characters[index]) { index += 1 } guard index > exponentStart else { return nil } } guard let number = Double(String(characters[start.. Double { guard value.isFinite else { if error == nil { error = message } return 0 } return value } mutating func sum(depth: Int) throws -> Double { var value = try product(depth: depth) while current == .plus || current == .minus { let subtract = current == .minus index += 1 isExpression = true let rhs = try product(depth: depth) value = checked(subtract ? value - rhs : value + rhs) } return value } mutating func product(depth: Int) throws -> Double { var value = try unary(depth: depth) while current == .multiply || current == .divide || current == .modulo || current == .percent || current == .of || current == .off { let operation = current if operation == .of || operation == .off { guard index > 0, tokens[index - 1] == .percent else { throw ParseError.invalid } } index += 1 isExpression = true let rhs = operation == .off ? try sum(depth: depth + 1) : try unary(depth: depth) switch operation { case .multiply, .of: value = checked(value * rhs) case .off: value = checked(rhs * (1 - value)) case .divide: if rhs == 0 { if error == nil { error = "Division by zero" }; value = 0 } else { value = checked(value / rhs) } default: if rhs == 0 { if error == nil { error = "Division by zero" }; value = 0 } else { value = checked(value.truncatingRemainder(dividingBy: rhs)) } } } return value } mutating func unary(depth: Int) throws -> Double { guard depth < Calculator.maximumDepth else { throw ParseError.invalid } if current == .plus || current == .minus { let negate = current == .minus index += 1 let value = try unary(depth: depth + 1) return negate ? -value : value } return try power(depth: depth) } mutating func power(depth: Int) throws -> Double { var value = try primary(depth: depth) while current == .percent { // An unsigned operand after % means remainder; otherwise % is postfix percentage. if index + 1 < tokens.count { switch tokens[index + 1] { case .number, .name, .left: return try exponent(value, depth: depth) default: break } } index += 1 isExpression = true value = checked(value / 100) } return try exponent(value, depth: depth) } mutating func exponent(_ value: Double, depth: Int) throws -> Double { guard current == .power else { return value } index += 1 isExpression = true let rhs = try unary(depth: depth + 1) return checked(pow(value, rhs), message: "Power is outside its domain") } mutating func primary(depth: Int) throws -> Double { guard let token = current else { throw ParseError.invalid } index += 1 switch token { case .number(let value): return checked(value) case .left: guard depth < Calculator.maximumDepth else { throw ParseError.invalid } isExpression = true let value = try sum(depth: depth + 1) guard current == .right else { throw ParseError.invalid } index += 1 return value case .name(let name): isExpression = true if name == "pi" { return .pi } if name == "e" { return exp(1) } guard Calculator.functionNames.contains(name), current == .left, depth < Calculator.maximumDepth else { throw ParseError.invalid } index += 1 let argument = try sum(depth: depth + 1) guard current == .right else { throw ParseError.invalid } index += 1 let value: Double switch name { case "sqrt": value = sqrt(argument) case "abs": value = abs(argument) case "round": value = argument.rounded() case "floor": value = floor(argument) case "ceil": value = ceil(argument) case "sin": value = sin(argument) case "cos": value = cos(argument) case "tan": value = tan(argument) case "log": value = log10(argument) default: value = log(argument) } return checked(value, message: "\(name) is outside its domain") default: throw ParseError.invalid } } } private struct ConversionUnit { let family: String let symbol: String let dimension: Dimension? let scale: Double init(_ family: String, _ symbol: String, _ dimension: Dimension) { self.family = family self.symbol = symbol self.dimension = dimension self.scale = 1 } init(_ family: String, _ symbol: String, scale: Double) { self.family = family self.symbol = symbol self.dimension = nil self.scale = scale } private static let dataPrefixes: [String: Double] = [ "": 1, "k": 1e3, "K": 1e3, "M": 1e6, "G": 1e9, "T": 1e12, "Ki": 1024, "Mi": 1_048_576, "Gi": 1_073_741_824, "Ti": 1_099_511_627_776, ] private static let namedDataPrefixes: [String: (String, Double)] = [ "": ("", 1), "kilo": ("K", 1e3), "mega": ("M", 1e6), "giga": ("G", 1e9), "tera": ("T", 1e12), "kibi": ("Ki", 1024), "mebi": ("Mi", 1_048_576), "gibi": ("Gi", 1_073_741_824), "tebi": ("Ti", 1_099_511_627_776), ] private static let gallonLiters = UnitVolume.gallons.converter.baseUnitValue(fromValue: 1) // Native Fahrenheit coefficients leave 32 °F slightly above zero; preserve the defining ratio. private static let fahrenheit = UnitTemperature( symbol: "°F", converter: UnitConverterLinear(coefficient: 5.0 / 9.0, constant: 273.15 - 32.0 * 5.0 / 9.0) ) private static func usVolume(_ symbol: String, subdivisions: Double) -> ConversionUnit { // Foundation's cup is a 240 mL legal cup; these are consistent US customary gallon subdivisions. .init("volume", symbol, UnitVolume( symbol: symbol, converter: UnitConverterLinear(coefficient: gallonLiters / subdivisions) )) } static func parse(_ text: String) -> ConversionUnit? { let text = text.split(whereSeparator: \.isWhitespace).joined(separator: " ") // Keep the final b/B case-sensitive: bits and bytes differ by a factor of eight. if let suffix = text.last, suffix == "b" || suffix == "B" { let prefix = String(text.dropLast()) if let scale = dataPrefixes[prefix] { return ConversionUnit("data", text, scale: suffix == "b" ? scale / 8 : scale) } } let lowercase = text.lowercased() for suffix in ["bytes", "byte", "bits", "bit"] where lowercase.hasSuffix(suffix) { if let (prefix, scale) = namedDataPrefixes[String(lowercase.dropLast(suffix.count))] { let bytes = suffix.hasPrefix("byte") return .init("data", prefix + (bytes ? "B" : "b"), scale: bytes ? scale : scale / 8) } } switch lowercase { case "mm", "millimeter", "millimeters", "millimetre", "millimetres": return .init("length", "mm", UnitLength.millimeters) case "cm", "centimeter", "centimeters", "centimetre", "centimetres": return .init("length", "cm", UnitLength.centimeters) case "m", "meter", "meters", "metre", "metres": return .init("length", "m", UnitLength.meters) case "km", "kilometer", "kilometers", "kilometre", "kilometres": return .init("length", "km", UnitLength.kilometers) case "in", "inch", "inches": return .init("length", "in", UnitLength.inches) case "ft", "foot", "feet": return .init("length", "ft", UnitLength.feet) case "yd", "yard", "yards": return .init("length", "yd", UnitLength.yards) case "mi", "mile", "miles": return .init("length", "mi", UnitLength.miles) case "mg", "milligram", "milligrams": return .init("mass", "mg", UnitMass.milligrams) case "g", "gram", "grams": return .init("mass", "g", UnitMass.grams) case "kg", "kilogram", "kilograms": return .init("mass", "kg", UnitMass.kilograms) case "oz", "ounce", "ounces": return .init("mass", "oz", UnitMass.ounces) case "lb", "lbs", "pound", "pounds": return .init("mass", "lb", UnitMass.pounds) case "c", "°c", "celsius", "degrees celsius": return .init("temperature", "°C", UnitTemperature.celsius) case "f", "°f", "fahrenheit", "degrees fahrenheit": return .init("temperature", "°F", fahrenheit) case "k", "°k", "kelvin", "kelvins": return .init("temperature", "K", UnitTemperature.kelvin) case "ml", "milliliter", "milliliters", "millilitre", "millilitres": return .init("volume", "mL", UnitVolume.milliliters) case "l", "liter", "liters", "litre", "litres": return .init("volume", "L", UnitVolume.liters) case "tsp", "teaspoon", "teaspoons", "us tsp", "us teaspoon", "us teaspoons": return usVolume("tsp", subdivisions: 768) case "tbsp", "tablespoon", "tablespoons", "us tbsp", "us tablespoon", "us tablespoons": return usVolume("tbsp", subdivisions: 256) case "fl oz", "floz", "fluid ounce", "fluid ounces", "us fl oz", "us fluid ounce", "us fluid ounces": return usVolume("fl oz", subdivisions: 128) case "cup", "cups", "us cup", "us cups": return usVolume("cup", subdivisions: 16) case "pt", "pint", "pints", "us pint", "us pints": return usVolume("pt", subdivisions: 8) case "qt", "quart", "quarts", "us quart", "us quarts": return usVolume("qt", subdivisions: 4) case "gal", "gallon", "gallons", "us gallon", "us gallons": return .init("volume", "gal", UnitVolume.gallons) case "ms", "millisecond", "milliseconds": return .init("duration", "ms", scale: 0.001) case "s", "sec", "secs", "second", "seconds": return .init("duration", "s", scale: 1) case "min", "mins", "minute", "minutes": return .init("duration", "min", scale: 60) case "h", "hr", "hrs", "hour", "hours": return .init("duration", "h", scale: 3600) case "d", "day", "days": return .init("duration", "day", scale: 86400) case "wk", "wks", "week", "weeks": return .init("duration", "week", scale: 604800) case "m/s", "mps", "meter per second", "meters per second", "metre per second", "metres per second": return .init("speed", "m/s", UnitSpeed.metersPerSecond) case "km/h", "km/hr", "kph", "kmph", "kilometer per hour", "kilometers per hour", "kilometre per hour", "kilometres per hour": return .init("speed", "km/h", UnitSpeed.kilometersPerHour) case "mph", "mi/h", "mile per hour", "miles per hour": return .init("speed", "mph", UnitSpeed.milesPerHour) case "m²", "m2", "sqm", "sq m", "square m", "square meter", "square meters", "square metre", "square metres": return .init("area", "m²", UnitArea.squareMeters) case "cm²", "cm2", "sqcm", "sq cm", "square cm", "square centimeter", "square centimeters", "square centimetre", "square centimetres": return .init("area", "cm²", UnitArea.squareCentimeters) case "km²", "km2", "sqkm", "sq km", "square km", "square kilometer", "square kilometers", "square kilometre", "square kilometres": return .init("area", "km²", UnitArea.squareKilometers) case "ft²", "ft2", "sqft", "sq ft", "square ft", "square foot", "square feet": return .init("area", "ft²", UnitArea.squareFeet) case "in²", "in2", "sqin", "sq in", "square in", "square inch", "square inches": return .init("area", "in²", UnitArea.squareInches) default: return nil } } } }