evaluator

main

Evaluator is a lightweight and efficient Swift library for parsing and evaluating expressions written as strings.
jaeggerr/evaluator

Evaluator

Overview

Evaluator is a Swift library that allows parsing and evaluating expressions provided as strings. It was initially inspired by Expression, a library that was incredibly useful but lacked support for short-circuit operations, which was a critical requirement for my use case. Additionally, I wanted more robust type handling instead of relying heavily on Any.

Evaluator is cross-platform and has no dependencies, making it lightweight and easy to integrate into any Swift project.

Features

  • Supports mathematical operations (+, -, *, /, %)
  • Supports logical operations (&&, ||, !)
  • Supports comparison operations (==, !=, <, <=, >, >=)
  • Supports bitwise operations (&, |)
  • Supports custom variables prefixed with # or $, which accept _ and .
  • Supports custom functions with dynamic arguments
  • Supports short-circuit evaluation for logical operations
  • Supports arrays within variables
  • Provides strong type conversion using custom Swift protocols
  • Works with custom structs and classes via protocol conformance

Installation

Evaluator is available through Swift Package Manager (SPM). To install it, add the following to your Package.swift:

.package(url: "https://github.com/jaeggerr/evaluator.git", from: "1.1.2")

Or, if using Xcode, go to File > Swift Packages > Add Package Dependency and enter the repository URL.

Usage

Basic Expressions

Here are some examples of expressions that Evaluator can handle:

try ExpressionEvaluator.evaluate(expression: "5 + 3 * 2") // Returns 11
try ExpressionEvaluator.evaluate(expression: "(10 - 4) / 2") // Returns 3.0
try ExpressionEvaluator.evaluate(expression: "true && false") // Returns false
try ExpressionEvaluator.evaluate(expression: "5 > 3 && 10 < 20") // Returns true

Variables

Variables must be prefixed with # or $. They support underscores (_) and dots (.) to allow structured names.

let result = try ExpressionEvaluator.evaluate(expression: "$my_var + 10", variables: { name in
    switch name {
    case "$my_var": return 5
    default: throw ExpressionError.variableNotFound(name)
    }
})
// result == 15

Using Dotted Notation

let result = try ExpressionEvaluator.evaluate(expression: "#user.score * 2", variables: { name in
    switch name {
    case "#user.score": return 10
    default: throw ExpressionError.variableNotFound(name)
    }
})
// result == 20

Arrays

Evaluator supports arrays in variables, allowing indexed access.

let result = try ExpressionEvaluator.evaluate(expression: "#values[2] + 1", variables: { name in
    switch name {
    case "#values": return [10, 20, 30, 40]
    default: throw ExpressionError.variableNotFound(name)
    }
})
// result == 31

Supported Functions

Evaluator provides a set of built-in mathematical functions, which can be used in expressions:

Function Description Example
sqrt(x) Square root sqrt(4) = 2.0
floor(x) Rounds down to the nearest integer floor(3.7) = 3.0
ceil(x) Rounds up to the nearest integer ceil(3.2) = 4.0
round(x) Rounds to the nearest integer round(3.5) = 4.0
cos(x) Cosine (radians) cos(0) = 1.0
acos(x) Arc cosine acos(1) = 0.0
sin(x) Sine (radians) sin(0) = 0.0
asin(x) Arc sine asin(0) = 0.0
tan(x) Tangent (radians) tan(0) = 0.0
atan(x) Arc tangent atan(1) = π/4
log(x) Natural logarithm log(e) = 1.0
abs(x) Absolute value abs(-5) = 5.0
pow(x, y) Power function pow(2, 3) = 8.0
atan2(y, x) Two-argument arc tangent atan2(1, 1) = π/4
max(a, b, ...) Maximum value max(3, 5, 2) = 5.0
min(a, b, ...) Minimum value min(3, 5, 2) = 2.0

Overriding Built-in Functions

You can override any built-in function by defining a custom function with the same name. Custom functions take precedence over built-in functions.

let customFunctions: ExpressionEvaluator.FunctionResolver = { name, args in
    switch name {
    case "sqrt":
        return "Overridden sqrt function!"
    default:
        throw ExpressionError.functionNotFound(name)
    }
}

let result = try ExpressionEvaluator.evaluate(expression: "sqrt(4)", functions: customFunctions)
print(result) // "Overridden sqrt function!"

Custom Functions

Evaluator allows defining custom functions that can be called within an expression.

let result = try ExpressionEvaluator.evaluate(expression: "sum(1, 2, 3)", functions: { name, args in
    switch name {
    case "sum": return args.reduce(0, { ($0 as! Int) + ($1 as! Int) })
    default: throw ExpressionError.functionNotFound(name)
    }
})
// result == 6

Type Conversion

Evaluator supports custom types via protocol conformance. If an unknown type is encountered, it tries to convert it into a suitable type.

Custom Struct Example

If you have a custom struct IntWrapper, you can make it compatible by conforming to EvaluatorIntConvertible:

struct IntWrapper: EvaluatorIntConvertible {
    let value: Int
    func convertToInt() throws -> Int {
        return value
    }
}

Then, use it in an expression:

let result = try ExpressionEvaluator.evaluate(expression: "$wrapped + 2", variables: { name in
    switch name {
    case "$wrapped": return IntWrapper(value: 8)
    default: throw ExpressionError.variableNotFound(name)
    }
})
// result == 10

Generic Return Type Handling

The return type of evaluate is determined by the generic parameter T.

let intResult: Int = try ExpressionEvaluator.evaluate(expression: "5 + 2") // Returns Int
let doubleResult: Double = try ExpressionEvaluator.evaluate(expression: "5 / 2") // Returns Double
let boolResult: Bool = try ExpressionEvaluator.evaluate(expression: "5 > 2") // Returns Bool

Supported return types:

  • Int, Int8, Int16, Int32, Int64
  • UInt8, UInt16, UInt32, UInt64
  • Double, Float
  • Bool
  • String

Custom Comparators

Evaluator allows defining custom comparison behavior via the ComparatorResolver. This is useful for handling user-defined types.

Example: Comparing Custom Structs

struct CustomObject {
    let score: Int
}

let comparator: ExpressionEvaluator.ComparatorResolver = { lhs, rhs, op in
    guard let lhsObj = lhs as? CustomObject, let rhsObj = rhs as? CustomObject else {
        throw ExpressionError.typeMismatch("Cannot compare these types")
    }
    return op.compare(lhs: lhsObj.score, rhs: rhsObj.score)
}

Then, pass it into evaluate:

let result = try ExpressionEvaluator.evaluate(expression: "$obj1 > $obj2", variables: { name in
    switch name {
    case "$obj1": return CustomObject(score: 50)
    case "$obj2": return CustomObject(score: 30)
    default: throw ExpressionError.variableNotFound(name)
    }
}, comparator: comparator)
// result == true

Function Argument Helper

A helper struct ArgumentsHelper has been added to simplify function argument parsing. This helps enforce correct argument types and arities when defining custom functions.

Usage

let functions: ExpressionEvaluator.FunctionResolver = {
    switch $0 {
    case "multiply":
        let argsHelper = ArgumentsHelper($1)
        let a: Double = try argsHelper.get(0)
        let b: Double = try argsHelper.get(1)
        return a * b
    default: throw ExpressionError.functionNotFound($0)
    }
}
let result: Double = try ExpressionEvaluator.evaluate(expression: "multiply(3, 4)", functions: functions)
print(result) // Output: 12.0

Features of ArgumentsHelper

  • Ensures correct number of arguments with ensureArity()
  • Retrieves arguments safely with type checks
  • Supports Double, Int, String, Bool, and generic types

License

This library is licensed under a standard open-source license that allows modifications and closed-source usage but does not permit reselling the library.

Contributions

Contributions and suggestions are welcome on a case-by-case basis.

Unit Tests

Unit tests are included to ensure correctness and reliability.

Description

  • Swift Tools 5.7.0
View More Packages from this Author

Dependencies

  • None
Last updated: Thu May 15 2025 03:07:00 GMT-0900 (Hawaii-Aleutian Daylight Time)