GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

SOLVED: Trapping NSExpression erorrs

Forums > Swift

I'm allowing the user to type in an expression and, as they type, trying to display the current result. I calculate the result by creating an NSExpression and evaluating it:

let expression = NSExpression(format: formula)
let  interimResult = expression.expressionValue(with: nil, context: nil) as? Int ?? 0

This works provided the formula is valid. However, there will be (many) times when the formula isn't valid (such as 2 +). When this is the case, NSExpression throws an NSInvalidArgumentException. I would like to trap this and adjust the UI to indicate that the expression isn't valid.

I'm failing.

I have tried putting the creation and evaluation of the expresison in a do catch block:

        do {
            let expression = NSExpression(format: formula)
            let interimResult = expression.expressionValue(with: nil, context: nil) as? Int ?? 0
        } catch {
            print(error.localizedDescription)
        }

The exception is still thrown and the console shows an uncaught exception. The catch block isn't entered. So, I moved the do/catch up a level to the calling function:

    var formula: String = "" {
        didSet {
            interimResult = 0
            do {
                validateFormula()
            } catch {
                print(error.localizedDescription)
            }
        }
    }

This, again, throws the NSInvalidArgumentException and the catch block is never entered.

Am I missing something obvious here? Is there no way to catch NSExpression exceptions?

I could just ignore the error (which is what I am currently doing) but that feels wrong.

Thanks Steve

   

Hello,

It seems like an interesting project you are working on.

If I understand your need correctly, have you looked at alternative solutions to NSExpression? 🤔

To avoid problems with Objective-C exceptions and to achieve a more modern and secure solution, I recommend using a Swift-based library like Expression. This will make your code easier to maintain and your user interface more responsive and robust against incorrect input. Github: A cross-platform Swift library for evaluating mathematical expressions at runtime

I'm no NSExpression expert, but could the following work for you? You can use try? to handle errors more gracefully (without needing a do-catch block):

func evaluateFormulaSafely(formula: String) -> Int {
    guard let expression = try? NSExpression(format: formula) else {
        print("Invalid formula format")
        return 0
    }
    return expression.expressionValue(with: nil, context: nil) as? Int ?? 0
}

   

I did try using try? but that was ignored and the exception was thrown anyway. It was very frustrating indeed.

I try, as much as possible, to avoid third party code. However, in this case, I may well have to resort to using Expression. It's overkill for my simple needs, but definitely represents a neater solution.

Thank you for the recommendation. Steve

1      

Hi Steve,

As I said I have never used NCExpression. So it feels like I'm in deep water when I write this answer. I'll try to stay afloat... ☺️

After some searching, can this way work for you?

func evaluateFormula(formula: String) -> Int {
    do {
        // Wrap the call in a do-catch to catch NSExceptions
        let expression = try NSExpression(format: formula)
        let interimResult = expression.expressionValue(with: nil, context: nil) as? Int ?? 0
        return interimResult
    } catch let exception as NSException {
        // Catch NSException errors specifically
        print("Caught exception: \(exception.name), Reason: \(exception.reason ?? "No reason")")
        return 0  // Return a default or fallback value
    }
}

Catching NSException directly: The key part here is that NSException is an Objective-C exception, not a Swift error. By using catch let exception as NSException, you can catch these exceptions in Swift and deal with them appropriately.

🫣 Does it work? Greetings Martin ⛵️

   

That's not going to work because the init for NSExpression isn't marked as a throwing function so you can't use try with it.

I don't believe you're going to be able to trap on this error because Swift won't catch Objective-C exceptions, and that's what this is. It's deep in the API and Swift just won't catch those.

Now, I could be wrong, but I'm fairly confident you aren't going to find one weird trick to solve this.

1      

I think you're right. Nothing I have tried works. I'm going to have to re-think this.

I suspect my simplest option is to apply some validation to the formula (right number of values, brackets and operators) before I try to evaluate the formula. It wont eliminate every error, but should greatly reduce them.

Thanks both

1      

On reflection, my needs are very simple (basic math functions with support for brackets) and I need Integer maths, so I'm going to write my own evaluation code. Saves me having to deal with the overhead of a large third party package or the complexities of Objective-C wrappers (I did find some code on GitHub that wraps NSExpression and handles the exceptions).

Just for the fun of it, I have used ChatGPT to write me starter code which look like it might even compile, though that would be a first!

1      

This is the simple code I ended up with. It works for my needs in that it can evaluate expressions using + - * and / and supports the use of brackets. As coded, it uses integer artihmetic, but the typealias can be changed to Double if you want doubles. Not saying it's bullet proof, but it's working for me. Maybe someone will find it a useful starter for a more comprehensive evaluation mechanism.


import Foundation

enum EvaluationErrors: Error {
    case invalidCharacter(char: Character)
    case unknownOperator(op: Character)
    case divideByZero
    case unexpectedToken
    case incompleteFormula2
}

struct FormulaEvaluator {

    typealias resultType = Int

    private enum Token: Equatable {
        case number(resultType)
        case operatorSymbol(Character)
        case leftParenthesis
        case rightParenthesis
    }

    /// Evaluate an expression and return the result of that expression.
    ///
    /// - Parameter expression: The expression as a plain text string
    /// - Returns: Te result of calculating the expression
    ///
    /// - Throws:
    ///     InvalidCharacter - when an invalidf character is found
    ///     unknownOperator - when an operator is found that is not one of our valid operators
    ///     divideByZero - if the expression includes a divide by operation and the divisor is zero
    ///     unexpectedToken - if the formula is malformed
    ///     incompleteFormula - if the formula is incomplete. e.g. 3+
    ///
    public func evaluate(expression: String) throws -> resultType {
        /*
         How this works...

         Suppose we start with a formula consisting of "(1 + 2 * 3) / 4". The call to
         tokenize will return an arry of the tokenized parts of the formula:

         [0] = leftParenthesis
         [1] = number (number = 1)
         [2] = operatorSymbol (operatorSymbol = "+")
         [3] = number (number = 2)
         [4] = operatorSymbol (operatorSymbol = "*")
         [5] = number (number = 3)
         [6] = rightParenthesis
         [7] = operatorSymbol (operatorSymbol = "/")
         [8] = number (number = 4)

         We then run this through the infixToPostfix conversion that constructs
         the Postfix array, showing the order in which we want to evaluate the
         tokens:

         [0] = number (number = 1)
         [1] = number (number = 2)
         [2] = number (number = 3)
         [3] = operatorSymbol (operatorSymbol = "*")
         [4] = operatorSymbol (operatorSymbol = "+")
         [5] = number (number = 4)
         [6] = operatorSymbol (operatorSymbol = "/")

         Finally, we evaluate the expression in it's postfix form.
        */
        let tokens = try tokenize(expression: expression)
        let postfixTokens = infixToPostfix(tokens: tokens)
        return try evaluatePostfix(postfixTokens)
    }

    private func tokenize(expression: String) throws -> [Token] {
        var tokens = [Token]()
        var currentNumber = ""

        for char in expression {
            if char.isNumber || char == "." {
                currentNumber.append(char)
            } else {
                if !currentNumber.isEmpty {
                    tokens.append(.number(resultType(currentNumber)!))
                    currentNumber = ""
                }

                switch char {
                case "+", "-", "*", "/":
                    tokens.append(.operatorSymbol(char))
                case "(":
                    tokens.append(.leftParenthesis)
                case ")":
                    tokens.append(.rightParenthesis)
                case " ":
                    continue  // Ignore spaces
                default:
                    throw EvaluationErrors.invalidCharacter(char: char)
                }
            }
        }

        // Add the last collected number if any
        if !currentNumber.isEmpty {
            tokens.append(.number(resultType(currentNumber)!))
        }

        return tokens
    }

    private func precedence(of operatorSymbol: Character) -> Int {
        switch operatorSymbol {
        case "+", "-":
            return 1
        case "*", "/":
            return 2
        default:
            return 0
        }
    }

    private func infixToPostfix(tokens: [Token]) -> [Token] {
        var outputQueue = [Token]()
        var operatorStack = [Token]()

        for token in tokens {
            switch token {
            case .number:
                outputQueue.append(token)
            case .operatorSymbol(let op):
                while let last = operatorStack.last, case .operatorSymbol(let lastOp) = last,
                      precedence(of: lastOp) >= precedence(of: op) {
                    outputQueue.append(operatorStack.popLast()!)
                }
                operatorStack.append(token)
            case .leftParenthesis:
                operatorStack.append(token)
            case .rightParenthesis:
                while let last = operatorStack.last,
                        last != .leftParenthesis {
                    outputQueue.append(operatorStack.popLast()!)
                }
                let _ = operatorStack.popLast()  // Remove the left parenthesis
            }
        }

        while let last = operatorStack.popLast() {
            outputQueue.append(last)
        }

        return outputQueue
    }

    private func evaluatePostfix(_ tokens: [Token]) throws -> resultType {
        var stack = [resultType]()

        for token in tokens {
            switch token {
            case .number(let value):
                stack.append(value)
            case .operatorSymbol(let op):
                guard let right = stack.popLast() else { throw EvaluationErrors.incompleteFormula }
                guard let left = stack.popLast() else { throw EvaluationErrors.incompleteFormula }
                let result: resultType
                switch op {
                case "+":
                    result = left + right
                case "-":
                    result = left - right
                case "*":
                    result = left * right
                case "/":
                    if right == 0 { throw EvaluationErrors.divideByZero }
                    result = left / right
                default:
                    throw EvaluationErrors.unknownOperator(op: op)
                }
                stack.append(result)
            default:
                throw EvaluationErrors.unexpectedToken
            }
        }

        return stack.popLast()!
    }
}

1      

Hacking with Swift is sponsored by Alex.

SPONSORED Alex is the iOS & Mac developer’s ultimate AI assistant. It integrates with Xcode, offering a best-in-class Swift coding agent. Generate modern SwiftUI from images. Fast-apply suggestions from Claude 3.5 Sonnet, o3-mini, and DeepSeek R1. Autofix Swift 6 errors and warnings. And so much more. Start your 7-day free trial today!

Try for free!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.