Git Product home page Git Product logo

expression's Introduction

PayPal Build Codecov Platforms Swift 4.2 Swift 5.1 License Mastodon

Introduction

What?

Expression is a Swift framework for evaluating expressions at runtime on Apple and Linux platforms

The Expression library is split into two parts:

  1. The Expression class, which is similar to Foundation's built-in NSExpression class, but with better support for custom operators, a more Swift-friendly API, and superior performance.

  2. AnyExpression, an extension of Expression that handles arbitrary types and provides additional built-in support for common types such as String, Dictionary, Array and Optional.

Why?

There are many situations where it is useful to be able to evaluate a simple expression at runtime. Some are demonstrated in the example apps included with the library:

  • A scientific calculator
  • A CSS-style color string parser
  • A basic layout engine, similar to AutoLayout

But there are other possible applications, e.g.

  • A spreadsheet app
  • Configuration (e.g. using expressions in a config file to avoid data duplication)
  • The basis for simple scripting language

(If you find any other use cases, let me know and I'll add them)

Normally these kind of calculations would involve embedding a heavyweight interpreted language such as JavaScript or Lua into your app. Expression avoids that overhead, and is also more secure as it reduces the risk of arbitrary code injection or crashes due to infinite loops, buffer overflows, etc.

Expression is fast, lightweight, well-tested, and written entirely in Swift. It is substantially faster than using JavaScriptCore for evaluating simple expressions (see the Benchmark app for a scientific comparison.

How?

Expression works by parsing an expression string into a tree of symbols, which can then be evaluated at runtime. Each symbol maps to a Swift closure (function) which is executed during evaluation. There are built-in functions representing common math operations, or you can provide your own custom ones.

Although the Expression class only works with Double values, AnyExpression uses a technique called NaN boxing to reference arbitrary data via the unused bit patterns in the IEEE floating point specification.

Usage

Installation

The Expression class is encapsulated in a single file, and everything public is prefixed or name-spaced, so you can simply drag the Expression.swift file into your project to use it. If you wish to use the AnyExpression extension then include the AnyExpression.swift file as well.

If you prefer, there's a framework that you can import which includes both the Expression and AnyExpression classes. You can install this manually by drag and drop, or automatically using CocoaPods, Carthage, or Swift Package Manager.

To install Expression using CocoaPods, add the following to your Podfile:

pod 'Expression', '~> 0.13'

To install using Carthage, add this to your Cartfile:

github "nicklockwood/Expression" ~> 0.13

To install using Swift Package Manager, add this to the dependencies: section in your Package.swift file:

.package(url: "https://github.com/nicklockwood/Expression.git", .upToNextMinor(from: "0.13.0")),

Integration

You create an Expression instance by passing a string containing your expression, and (optionally) any or all of the following:

  • A set of configuration options - used to enabled or disable certain features
  • A dictionary of named constants - this is the simplest way to specify predefined constants
  • A dictionary of named array constants - this is the simplest way to specify predefined arrays of related values
  • A dictionary of symbols and SymbolEvaluator functions - this allows you to provide custom variables, functions or operators

You can then calculate the result by calling the evaluate() method.

Note: The evaluate() function for a given Expression or AnyExpression instance is thread-safe, meaning that you can call it concurrently from multiple threads.

By default, Expression already implements most standard math functions and operators, so you only need to provide a custom symbol dictionary if your app needs to support additional functions or variables. You can mix and match implementations, so if you have some custom constants or arrays and some custom functions or operators, you can provide separate constants and symbols dictionaries.

Here are some examples:

// Basic usage:
// Only using built-in math functions

let expression = Expression("5 + 6")
let result = try expression.evaluate() // 11

// Intermediate usage:
// Custom constants, variables and  and functions

var bar = 7 // variable
let expression = Expression("foo + bar + baz(5) + rnd()", constants: [
    "foo": 5,
], symbols: [
    .variable("bar"): { _ in bar },
    .function("baz", arity: 1): { args in args[0] + 1 },
    .function("rnd", arity: 0): { _ in arc4random() },
])
let result = try expression.evaluate()

// Advanced usage:
// Using the alternative constructor to dynamically hex color literals

let hexColor = "#FF0000FF" // rrggbbaa
let expression = Expression(hexColor, pureSymbols: { symbol in
    if case .variable(let name) = symbol, name.hasPrefix("#") { {
        let hex = String(name.characters.dropFirst())
        guard let value = Double("0x" + hex) else {
            return { _ in throw Expression.Error.message("Malformed color constant #\(hex)") }
        }
        return { _ in value }
    }
    return nil // pass to default evaluator
})
let color: UIColor = {
    let rgba = UInt32(try expression.evaluate())
    let red = CGFloat((rgba & 0xFF000000) >> 24) / 255
    let green = CGFloat((rgba & 0x00FF0000) >> 16) / 255
    let blue = CGFloat((rgba & 0x0000FF00) >> 8) / 255
    let alpha = CGFloat((rgba & 0x000000FF) >> 0) / 255
    return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}()

Note that the evaluate() function may throw an error. An error will be thrown automatically during evaluation if the expression was malformed, or if it references an unknown symbol. Your custom symbol implementations may also throw application-specific errors, as in the colors example above.

For a simple, hard-coded expression like the first example there is no possibility of an error being thrown, but if you accept user-entered expressions, you must always ensure that you catch and handle errors. The error messages produced by Expression are detailed and human-readable (but not localized).

do {
    let result = try expression.evaluate()
    print("Result: \(result)")
} catch {
    print("Error: \(error)")
}

When using the constants, arrays and symbols dictionaries, error message generation is handled automatically by the Expression library. If you need to support dynamic symbol decoding (as in the hex color example earlier), you can use the init(impureSymbols:pureSymbols) initializer, which is a little bit more complex.

The init(impureSymbols:pureSymbols) initializer accepts a pair of lookup functions that take a Symbol and return a SymbolEvaluator function. This interface is very powerful because it allows you to dynamically resolve symbols (such as the hex color constants in the colors example) without needing to create a dictionary of all possible values in advance.

For each symbol, your lookup functions can return either a SymbolEvaluator function, or nil. If you do not recognize a symbol, you should return nil so that it can be handled by the default evaluator. If neither lookup function matches the symbol, and it is not one of the standard math or boolean functions, evaluate() will throw an error.

In some cases you may recognize a symbol, but be certain that it is incorrect, and this is an opportunity to provide a more specific error message than Expression would generate by default. The following example matches a function bar with an arity of 1 (meaning that it takes one argument). This will only match calls to bar that take a single argument, and will ignore calls with zero or multiple arguments.

switch symbol {
case .function("bar", arity: 1):
    return { args in args[0] + 1 }
default:
    return nil // pass to default evaluator
}

Since bar is a custom function, we know that it should only take one argument, so it's more helpful to throw an error if it is called with the wrong number of arguments, rather than returning nil to indicate that the function doesn't exist. That would look something like this:

switch symbol {
case .function("bar", let arity):
    guard arity == 1 else {
        return { _ in throw Expression.Error.message("function bar expects 1 argument") }
    }
    return { arg in args[0] + 1 }
default:
    return nil // pass to default evaluator
}

Note: Newer versions of Expression can correctly report trivial arity errors like this anyway, so this is a slightly contrived example, but this approach may be useful for other types of error, such as when arguments are out of range, or the wrong type.

Symbols

Expressions are formed from a mixture of numeric literals and symbols, which are instances of the Expression.Symbol enum type. The built-in math and boolean libraries define a number of standard symbols, but you are free to define your own.

The Expression.Symbol enum supports the following symbol types:

Variables

.variable(String)

This is an alphanumeric identifier representing a constant or variable in an expression. Identifiers can be any sequence of letters and numbers, beginning with a letter, underscore (_), dollar symbol ($), at sign (@) or hash/pound sign (#).

Like Swift, Expression allows unicode characters in identifiers, such as emoji and scientific symbols. Unlike Swift, Expression's identifiers may also contain periods (.) as separators, which is useful for name-spacing (as demonstrated in the Layout example app).

The parser also accepts quoted strings as identifiers. Single quotes, double quotes, or backticks may be used. Since Expression only deals with numeric values, it's up to your application to map these string identifiers to numbers (if you are using AnyExpression then this is handled automatically).

Unlike regular identifiers, quoted identifiers can contain any unicode character, including spaces. Newlines, quotes and other special characters can be escaped using a backslash (). Escape sequences are decoded for you, but the outer quotes are retained so you can distinguish strings from other identifiers.

Finally, unquoted identifiers are permitted to end with a single quote ('), as this is a common notation used in mathematics to indicate modified values. A quote at any other point in the identifier will be treated as the end of the name.

To verify that a given string is safe for use as an identifier, you can use the Expression.isValidIdentifier() method.

Operators

.infix(String)
.prefix(String)
.postfix(String)

These symbols represent operators. Operators can be one or more characters long, and can contain almost any symbol that doesn't conflict with a valid identifier name, with some caveats:

  • Comma (,) is a valid operator on its own, but cannot form part of a longer character sequence

  • The bracket characters [, '(', '{', and their counterparts are reserved and cannot be used as operators

  • An operator may begin with one or more dots (.) or hyphens (-), but a dot or hyphen cannot appear after any other character. The following are permitted:

    ..., ..<, ., -, --, -=, -->

    but the following are not:

    +., >.<, *-, -+-, <--, .-, -.

To verify that a given character sequence is safe for use as an operator, you can use the Expression.isValidOperator() method.

You can overload existing infix operators with a post/prefix variant, or vice-versa. Disambiguation depends on the white-space surrounding the operator (which is the same approach used by Swift).

Any valid identifier may also be used as an infix operator, by placing it between two operands, or as a postfix operator, by placing it after an operand. For example, you could define m and cm as postfix operators when handling distance logic, or use and as a more readable alternative to the boolean && operator.

Operator precedence follows standard BODMAS order, with multiplication/division given precedence over addition/subtraction. Prefix operators take precedence over postfix operators, which take precedence over infix ones. There is currently no way to specify precedence for custom operators - they all have equal priority to addition/subtraction.

Standard boolean operators are supported, and follow the normal precedence rules, with the caveat that short-circuiting (where the right-hand argument(s) may not be evaluated, depending on the left-hand-side) is not supported. The parser will also recognize the ternary ?: operator, treating a ? b : c as a single infix operator with three arguments.

Functions

.function(String, arity: Arity)

A function symbol is defined with a name and an Arity, which is the number of arguments that it expects. The Arity type is an enum that can be set to either exactly(Int) or atLeast(Int) for variadic functions. A given function name can be overloaded multiple times with different arities.

Note: Arity conforms to ExpressibleByIntegerLiteral, so for fixed-arity functions you can just write .function("foo", arity: 2) instead of .function("foo", arity: .exactly(2))

Functions are called by using their name followed by a comma-delimited sequence of arguments in parentheses. If the argument count does not match any of the specified arity variants, an arityError will be thrown.

Since function symbols must have a name, it is not directly possible to use anonymous functions in an expression (e.g. functions that are stored in a variable, or returned by another function).

There is syntax support for this however, if you implement the function call operator .infix("()"), which accepts one or more arguments, with the first being treated as the function to be called. This is of limited use in Expression (where values are all numeric) but AnyExpression uses this approach to provide full support for [anonymous functions(#anonymous-functions).

Arrays

.array(String)

Array symbols represent a sequence of values that can be accessed by index. Array symbols are referenced in an expression by using their name followed by an index argument in square brackets.

The simplest way to use arrays with Expression is to pass in a constant array value via the arrays initializer argument. For variable arrays, you can return an .array() symbol implementation via the symbols argument.

Expression also supports Swift-style array literal syntax like [1, 2, 3] and subscripting of arbitrary expressions like (a + b)[c]. Array literals map to the array literal constructor symbol .function("[]", arity: .any) and subscripting maps to the array subscripting operator .infix("[]").

Because Expression cannot work with non-numeric types, neither the array literal constructor nor the array subscripting operator have default implementations in Expression, however both of these are implemented in AnyExpression's standard symbol library.

Performance

Caching

By default, Expression caches parsed expressions. The expression cache is unlimited in size. In most applications this is very unlikely to ever be a problem - expressions are tiny, and even the most complex expression you can imagine is probably well under 1KB, so it would take a lot of them to cause memory pressure - But if for some reason you do ever need to reclaim the memory used by cached expressions, you can do so by calling the flushCache() method:

Expression.flushCache())

The flushCache() method takes an optional string argument, so you can also remove a specific expression from the cache without clearing others:

Expression.flushCache(for: "foo + bar"))

If you'd prefer even more fine-grained control of caching, you can pre-parse the expression without caching it, then create the Expression instance from the pre-parsed expression, as follows:

let expressionString = "foo + bar"
let parsedExpression = Expression.parse(expressionString, usingCache: false)
let expression = Expression(parsedExpression, constants: ["foo": 4, "bar": 5])

By setting the usingCache argument to false in the code above, we avoid adding the expression to the global cache. You are also free to implement your own caching by storing the parsed expression and re-using it, which may be more efficient than the built-in cache in some cases (e.g. by avoiding thread management if your code is single-threaded).

A second variant of the Expression.parse() method accepts a String.UnicodeScalarView.SubSequence and optional list of terminating delimiter strings. This can be used to match an expression embedded inside a longer string, and leaves the startIndex of the character sequence in the right place to continue parsing once the delimiter is reached:

let expressionString = "lorem ipsum {foo + bar} dolor sit"
var characters = String.UnicodeScalarView.SubSequence(expression.unicodeScalars)
while characters.popFirst() != "{" {} // Read up to start of expression
let parsedExpression = Expression.parse(&characters, upTo: "}")
let expression = Expression(parsedExpression, constants: ["foo": 4, "bar": 5])

Optimization

By default, expressions are optimized where possible to make evaluation more efficient. Common optimizations include replacing constants with their literal values, and replacing pure functions or operators with their result when all arguments are constant.

The optimizer reduces evaluation time at the cost of increased initialization time, and for an expression that will only be evaluated once or twice this tradeoff may not be worth it, in which case you can disable optimization using the options argument:

let expression = Expression("foo + bar", options: .noOptimize, ...)

On the other hand, if your expressions are being evaluated hundreds or thousands of times, you will want to take full advantage of the optimizer to improve your application's performance. To ensure you are getting the best out of Expression's optimizer, follow these guidelines:

  • Always pass constant values via the constants or arrays arguments instead of as a variable in the symbols dictionary. Constant values can be inlined, whereas variables must be re-computed each time the function is evaluated in case they have changed.

  • If your custom functions and operators are all pure - i.e. they have no side effects and always return the same output for a given set of argument values - then you should set the pureSymbols option for your expression. This option tells the optimizer that it's safe to inline any functions or operators in the symbols dictionary if all their arguments are constant. Note that the pureSymbols option does not affect variables or array symbols, which are never inlined.

  • If your expressions may contain values which are constant, but where not all possible values can be computed in advance - e.g. encoded values such as in the hex colors example, or arbitrary key paths that must be looked up in a deep object graph - you can use the init(pureSymbols:) initializer to decode or look up just the specific values that are needed.

Standard Library

Math Symbols

By default, Expression supports a number of basic math functions, operators, and constants that are generally useful, independent of any particular application.

If you use a custom symbol dictionary, you can override any default symbol, or overload default functions with different numbers of arguments (arity). Any symbols from the standard library that you do not explicitly override will still be available.

To explicitly disable individual symbols from the standard library, you can override them and throw an exception:

let expression = Expression("pow(2,3)", symbols: [
    .function("pow", arity: 2): { _ in throw Expression.Error.undefinedSymbol(.function("pow", arity: 2)) }
])
try expression.evaluate() // this will throw an error because pow() has been undefined

If you are using the init(impureSymbols:pureSymbols:) initializer, you can fall back to the standard library functions and operators by returning nil for unrecognized symbols. If you do not want to provide access to the standard library functions in your expression, throw an error for unrecognized symbols instead of returning nil.

let expression = Expression("3 + 4", pureSymbols: { symbol in
    switch symbol {
    case .function("foo", arity: 1):
        return { args in args[0] + 1 }
    default:
        return { _ in throw Expression.Error.undefinedSymbol(symbol) }
    }
})
try expression.evaluate() // this will throw an error because no standard library operators are supported, including +

Here are the currently supported math symbols:

constants

pi

infix operators

+ - / * %

prefix operators

-

functions

// Unary functions

sqrt(x)
floor(x)
ceil(x)
round(x)
cos(x)
acos(x)
sin(x)
asin(x)
tan(x)
atan(x)
abs(x)
log(x)

// Binary functions

pow(x,y)
atan2(x,y)
mod(x,y)

// Variadic functions

max(x,y,[...])
min(x,y,[...])

Boolean Symbols

In addition to math, Expression also supports boolean logic, following the C convention that zero is false and any nonzero value is true. The standard boolean symbols are not enabled by default, but you can enable them using the .boolSymbols option:

let expression = Expression("foo ? bar : baz", options: .boolSymbols, ...)

As with the math symbols, all standard boolean operators can be individually overridden or disabled for a given expression using the symbols initializer argument.

Here are the currently supported boolean symbols:

constants

true
false

infix operators

==
!=
>
>=
<
<=
&&
||

prefix operators

!

ternary operator

?:

AnyExpression

Usage

AnyExpression is used in almost the exact same way as the Expression class, with the following exceptions:

  • AnyExpression's SymbolEvaluator functions accept and return Any instead of Double
  • Boolean symbols and operators are enabled by default when you create an AnyExpression
  • There is no separate arrays argument for the AnyExpression constructor. If you wish to pass an array or dictionary constant, you can add it to the constants dictionary like any other value type
  • AnyExpression supports [anonymous functions[(#anonymous-functions), which can be any value of type Expression.SymbolEvaluator or AnyExpression.SymbolEvaluator
  • You can also pass Expression.SymbolEvaluator or AnyExpression.SymbolEvaluator functions into AnyExpression using the constants dictionary, and these will behave just like ordinary function symbols

You can create and evaluate an AnyExpression instance as follows:

let expression = AnyExpression("'hello' + 'world'")
let result: String = try expression.evaluate() // 'helloworld'

Note the use of single quotes (') for string literals. AnyExpression supports single or double quotes for string literals. There is no difference between these, except that single quotes do not need to be escaped inside a Swift string literal.

Since AnyExpression's evaluate() method has a generic return type, you will need to tell it the expected type. In the example above, we did this by specifying an explicit type for the result variable, but you could also do it by using the as operator (without ! or ?):

let result = try expression.evaluate() as String

The evaluate function has a certain amount of built-in leniency with respect to types, so if (for example) the expression returns a boolean, but you specify Double as the expected type, the type will be converted automatically, but if it returns a string and you ask for Bool then a type mismatch error will be thrown.

The currently supported automatic conversions are:

  • T -> Optional
  • Numeric -> Numeric
  • Array -> Array
  • Numeric -> Bool
  • Bool -> Numeric
  • Any -> String

Symbols

In addition to adding support for string literals, AnyExpression extends Expression's standard library with some additional symbols for dealing with Optionals and null values:

  • nil - the null literal
  • ?? - the null coalescing operator

Optional unwrapping is automatic, so there is currently no need for the postfix ? or ! operators. nil (aka Optional.none) and NSNull are both treated the same way to avoid confusion when working with JSON or Objective-C API data.

Comparison operators like == and !=are also extended to work with anyHashabletype, and+` can be used for string concatenation, as in the example above.

For array symbols, AnyExpression can use any Hashable type as the index. This means that AnyExpression can work with Dictionary values as well as Arrays and ArraySlices.

Literals

As mentioned above, AnyExpression supports the use of quoted string literals, delimited with either single quotes (') or double quotes ("). Special characters inside the string can be escaped using a backlash ().

AnyExpression supports array literals defined in square brackets, e.g. [1, 2, 3] or ['foo', 'bar', 'baz']. Array literals can contain a mixture of value types and/or sub-expressions.

You can also create range literals using the ..< and ... syntaxes. Closed, half-open and partial ranges are supported. Ranges work with either Int or String.Index values, and can be used in conjunction with subscripting syntax for slicing arrays and strings.

Anonymous Functions

In addition to ordinary named function symbols, AnyExpression also calling anonymous functions, which are values of type Expression.SymbolEvaluator or AnyExpression.SymbolEvaluator that can be stored in a constant or returned from a sub-expression.

You can pass anonymous functions into AnyExpression by using a constant value instead of a .function() symbol, but note that this approach does not allow you the option of overloading functions with the same name by arity.

Unlike function symbols, anonymous functions do not support overloading, but you can use a switch inside the function body to implement different behaviors depending on the number of arguments. You should also throw an arityMismatch error if an unsupported number of arguments is passed, as this cannot be detected automatically, e.g.

let bar = { (args: [Any] throws -> Any in
    switch args.count {
    case 1:
        // behavior 1
    case 2:
        // behavior 2
    default:
        throw Expression.Error.arityMismatch(.function("bar", arity: 2))
    }
}

// static function foo returns anonymous function bar, which is called in the expression
let expression = AnyExpression("foo()(2)", symbols: [
    .function("foo"): { _ in bar }
])

Note: anonymous functions are assumed to be impure, so they are never eligible for inlining, regardless of whether you use the pureSymbols option.

Linux Support

AnyExpression works on Linux, with the following caveat:

  • AnyExpression doesn't support NSString bridging due to a limitation of Linux Foundation. If you want to use AnyExpression with NSString values then you'll have to manually convert them to String before and after evaluation.

Example Projects

Benchmark

The Benchmark app runs a set of test expressions using Expression, AnyExpression, NSExpression and JavaScriptCore's JSContext respectively. It then times how long it takes to parse and evaluate the expressions, and displays the result in a table.

Times are shown in either microseconds (ยตs) or milliseconds. The fastest result in each category is displayed in green, and the slowest in red.

For accurate results, the Benchmark app should be run in release mode on a real device. You can pull down on the table to refresh the test results. Tests are run on the main thread, so don't be surprised if the display locks up briefly while refreshing.

In my own tests, Expression was consistently the fastest implementation, and JavaScriptCore was consistently the slowest, both for initial setup and for evaluation once the context has been initialized.

Calculator

Not much to say about this. It's a calculator. You can type mathematical expressions into it, and it will evaluate them and produce a result (or an error, if what you typed was invalid).

Colors

The Colors example demonstrates how to use AnyExpression to create a (mostly) CSS-compliant color parser. It takes a string containing a named color, hex color or rgb() function call, and returns a UIColor object.

Layout

This is where things get interesting: The Layout example demonstrates a crude-but-usable layout system, which supports arbitrary expressions for the coordinates of the views.

It's conceptually similar to AutoLayout, but with some important differences:

  • The expressions can be as simple or as complex as you like. In AutoLayout every constraint uses a fixed formula, where only the operands are interchangeable.
  • Instead of applying an arbitrary number of constraints between properties of views, each view just has a size and position that can be calculated however you like.
  • Layout is deterministic. There is no weighting system used for resolving conflicts, and circular references are forbidden. Weighted relationships can be achieved using explicit multipliers.

Default layout values for the example views have been set in the Storyboard, but you can edit them live in the app by tapping a view and typing in new values.

Here are some things to note:

  • Every view has a top, left, width and height expression to define its coordinates on the screen.
  • Views have an optional key (like a tag, but string-based) that can be used to reference their properties from another view.
  • Any expression-based property of any view can reference any other property (of the same view, or any other view), and can even reference multiple properties.
  • Every view has a bottom and right property. These are computed, and cannot be set directly, but they can be used in expressions.
  • Circular references (a property whose value depends on itself) are forbidden, and will be detected by the system.
  • The width and height properties can use the auto variable, which does nothing useful for ordinary views, but can be used with text labels to calculate the optimal height for a given width, based on the amount of text.
  • Numeric values are measured in screen points. Percentage values are relative to the superview's width or height property.
  • Remember you can use functions like min() and max() to ensure that relative values don't go above or below a fixed threshold.

This is just a toy example, but if you like the concept check out the Layout framework on Github, which takes this idea to the next level.

REPL

The Expression REPL (Read Evaluate Print Loop) is a Mac command-line tool for evaluating expressions. Unlike the Calculator example, the REPL is based on AnyExpression, so it allows the use of any type that can be represented as a literal in Expression syntax - not just numbers.

Each line you type into the REPL is evaluated independently. To share values between expressions, you can define variables using an identifier name followed by = and then an expression, e.g:

foo = (5 + 6) + 7

The named variable ("foo", in this case) is then available to use in subsequent expressions.

Credits

The Expression framework is primarily the work of Nick Lockwood.

(Full list of contributors)

expression's People

Contributors

basthomas avatar carter-thaxton avatar flangel avatar foscomputerservices avatar frranck avatar maximkotliar avatar nicklockwood avatar nighthawk avatar traviskirton avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

expression's Issues

AnyExpression using array variables can't be evaluated with error 0

Hello,

Maybe I haven't read the readme well enough, but it mentions that for AnyExpression, I can just add a variable in the dictionary of constants as below, and explicitly cast the return. I'm not sure if the evaluation can consider say an array with a constant term (X + 1 where X say is an array), but I also tried something like X + X which would normally add the elements of X individually, like a numpy array addition. Maybe I'm being overtly optimistic with what Expression can do?

func expression(_ X: [CGFloat], _ expression: String) {
    let x = X.map{Double($0)}
    let expression = expression.lowercased()
    let eval = AnyExpression(expression, constants: ["x": x])
    do {
    let result:[CGFloat] = try eval.evaluate()
    print(result)
    } catch let error {
        print(error.localizedDescription)
    }
}

String literals/variables

Great expression tool.

Do you have plans to support string processing such as concatenation, length, substrings, etc. ?

If not, how would you suggest I handle this as strings are a component of the expressions I'm working with?

Trying to evaluate when the region of the device is set to french

Hi, I'm trying to evaluate a basic expression, like this one
2+3.5
so if I have the region set to some other country that the comma is the decimal separator, the method evaluate is throwing the error, not sure if there is a way to set the decimal separator
thanks in advance, you did a really great work with this library

Test suite crashes on Xcode 13.3 / Swift 5.6

Running this repo's test suite on macOS 12.3 with Xcode 13.3 and Swift 5.6 crashes on my M1 MacBook Air with an EXC_BAD_ACCESS.

This happens in tests such as testTooFewArgumentsForCustomFunctionWithAdvancedInitializer and a number of other ones which test a mismatching number of arguments.

#0	0x000000016fdfcd98 in 0x16fdfcd98 ()
#1	0x00000001078bc278 in thunk for @escaping @callee_guaranteed (@in_guaranteed [Double]) -> (@out Double, @error @owned Error) ()
#2	0x00000001078f92e0 in partial apply for thunk for @escaping @callee_guaranteed (@in_guaranteed [Double]) -> (@out Double, @error @owned Error) ()
#3	0x00000001078dfba8 in Subexpression.optimized(withImpureSymbols:pureSymbols:) at /Users/adrian/Development/forks/Expression/Sources/Expression.swift:892
#4	0x00000001078de7ac in Expression.init(_:impureSymbols:pureSymbols:) at /Users/adrian/Development/forks/Expression/Sources/Expression.swift:351
#5	0x00000001078de6e0 in Expression.__allocating_init(_:impureSymbols:pureSymbols:) ()
#6	0x00000001078dfe20 in Expression.__allocating_init(_:pureSymbols:) at /Users/adrian/Development/forks/Expression/Sources/Expression.swift:372
#7	0x000000010788b350 in ExpressionTests.testTooFewArgumentsForCustomFunctionWithAdvancedInitializer() at /Users/adrian/Development/forks/Expression/Tests/ExpressionTests.swift:934

I couldn't yet narrow down what might be causing it and if it's an issue introduced with Swift 5.6 or with this library, or what to do about it. It happens both when running through Xcode or with swift test from the command line. Any advise appreciated!

inaccurate result for 7+6.3+5.3+4.42

the result of 7+6.3+5.3+4.42 is 23.02, but what it returns is 23.020000000000003, I know it's because Double is not accurate enough for this result.

Expression.parse("'\\n'") fails

I've hit a funny little bug with this excellent library. I'm trying to evaluate an expression such as AnyExpression.parse(var1 + "'\\n'" + var2, ...), but this fails with an unexpectedToken("\\n\'"). If I modify it slightly to ``AnyExpression.parse(var1 + "' \n'" + var2, ...)` it works -- notice the additional space.

The simplest way to reproduce this is Expression.parse("'\\n'").

It seems to be an issue with parseEscapedIdentifier and in particular this line:

while let part = scanCharacters({ $0 != delimiter && $0 != "\\" }) {

Unfortunately this implementation is going a bit over my head, so I haven't yet figured out how to fix this. Any advice would be much appreciated.

Working with Vectors and Matrixes

Are there any plans for vectors/matrixes compatibility?
making this work:
sin([1, 2, 3])
--> [0.8414, 0.9092, 0.1411]

Same for matrixes. Basically changing evaluate() -> Double to either evaluate() -> T or evaluate() -> [Double]

Support ^ as pow operator

I don't think that pow(2, 3) is actually understood by anyone. Instead it feels more naturally to write 2^3.

Is it possible to support this using symbols? If not, would it be possible to include this as a standard operator?

Complex expressions are not working

I have an expression like this -> "(0.341 * 8000.0) / (1 - (1 + 0.341) ^ -84)" , I used below code but giving wrong answer.

Expected result - "109.71908469357446"
Actual result - "1.538906384258725e-36"

Can someone please correct me?

do {
            let result = try Expression(
                "(0.341 * 8000.0) / (1 - (1 + 0.341) ^ -84)",
                symbols: [
                    .infix("^"): { pow($0[0], $0[1]) }
                ]
            ).evaluate()

            print(result) 

        } catch {

        }

Why isn't "=" an operator?

Looking at your REPL example, I was wondering why you manually subdivide the expression around the "=" sign rather than making the "=" a proper operator, executing the same logic in its definition.

Maybe there is something I'm not understanding as to how the framework works.

Thanks.

Build failure on Swift 5.1 - partial application of 'mutating' method is not allowed

When attempting to build the library using Swift 5.1 the following error is generated.

/.../Sources/AnyExpression.swift:1096:72: error: partial application of 'mutating' method is not allowed
        return (AnyExpression.arrayCast(value) as [Element]?).map(self.init)

Steps to repeat:

  1. Install Swift 5.1 (or Xcode 11)
  2. git clone https://github.com/nicklockwood/Expression.git
  3. cd Expression
  4. swift build

The offending code is as follows:

extension ArraySlice: _SwiftArray {
    fileprivate var values: [Any] {
        return Array(self)
    }

    static func cast(_ value: Any) -> Any? {
        return (AnyExpression.arrayCast(value) as [Element]?).map(self.init) // <-- self.init fails to compile Swift 5.1
    }
}

Unexpected result related to whitespace and operators

I'm running into an issue where the absence of a whitespace character before a '+' operator in some cases has an affect on the result. For instance:

Expression("3 * 4 +5").evaluate() // 27.0
Expression("3 * 4 + 5").evaluate() // 17.0
Expression("3*4+5").evaluate() // 17.0

I believe this to be a bug.

Lazy evaluation of ternary operator's true/false cases

I've set up a bunch of expression functions and am hitting an issue where the ternary operator might check a condition and if that condition isn't met, the true part throws an error. Say I have a date: Date variable and a format/2 function that expects a Date and String and throws an error if the first parameter is not a Date. If I then use that in an expression like date != nil ? format(date, "yyyy-MM-dd") : "None" when date = nil, it'll throw that error rather than returning "None".

I could adjust my functions to not throw errors and return nil instead, but then I'm losing useful information when I use them elsewhere. And I'd have to change most functions to always accept nil for every parameter. Not ideal.

I'm wondering if it's possible to have the ternary operator evaluate the true/false cases lazily and, in particular, only evaluate the necessary part.

Or is there another possibility to address this? Feedback appreciated.

Crash in Swift 5.7 when creating stringBounds error

There's a crash in static func stringBounds(_ string: Substring, _ index: String.Index) -> AnyExpression.Error -> AnyExpression.Error happening on Swift 5.7 on macOS or Linux, which can be reproduced by running the Test Suite.

This happens for cases where index < _string.startIndex as let offset = _string.distance(from: _string.startIndex, to: index) will no longer return a negative value but instead crash at runtime as index is out of bounds of the string.

I tried a few approaches but couldn't figure out how to still get that correct negative value. Suggestion welcome! My workaround was to make stringBounds(_ string: String, _ index: Int) take instead an index: Int? and use generic wording in the error if no good index could be provided, but that's worse behaviour than what it did before...

Square brackets

Hello, I have problem with evaluating expression with square brackets.

In math it's OK to write [(10,3+10,1)*7]+3, but flee will not evaluate such expression..

Is there a way to make it work?

Thank you

P.S. of course I can replace square with round brackets, but it is not comfortable

Question: Overloading existing operators

I'm experimenting with using AnyExpression and its powerful support for custom data types and operators. This is working well, though I'm facing an issue when I want to use the same operator as defined by Expression (or AnyExpression) itself, but only use my overload for the custom data types and falling back to the default implementations otherwise.

Say, for example in have a "date" datatype which is really just Swift's Date, and I want to allow comparisons using <, <= and their friends, but if I define those as .infix("<") then this now works for my date types, but regular numeric expressions now fail.

I've tried following this from the README:

If you are using the init(impureSymbols:pureSymbols:) initializer, you can fall back to the standard library functions and operators by returning nil for unrecognized symbols.

However, I can't return nil at that stage, as I'd have to return nil from within the operator's SymbolEvaluator as it depends on the specific arguments.

Is this possible or do I have to reimplement the default behaviour in my custom operator?

Swift 5 support

Hi,

It seems like there is a warning on Swift 5:

Expression.swift:153:20: 'Hashable.hashValue' is deprecated as a protocol requirement; conform type 'Expression.Symbol' to 'Hashable' by implementing 'hash(into:)' instead

Thanks

Multiplication followed by dot followed by digit issue

Hello,

I'm getting the following error:
13:18:25 ERROR Error in expression X/((16.7-W)*.0000212*Y*Z): Undefined infix operator *.

I was hoping this commit would fix the issue: 08fef32 but it doesn't seems like it does.

Probably a simpler expression like 1*.5 would fails as well.

Hopefully this can be fixed somewhat easily.

Thank you

Function expression failed with string paramater which contains \

Here is the default test case, which the result is correct

func testCallAnyExpressionSymbolEvaluatorConstant() {
        let expression = AnyExpression("foo('foo', 'bar')", constants: [
            "foo": { ($0[0] as! String) + ($0[1] as! String) } as AnyExpression.SymbolEvaluator,
        ])
        XCTAssertEqual(try expression.evaluate(), "foobar")
        XCTAssertEqual(expression.symbols, [.function("foo", arity: 2)])
    }

Modify the parameter value a bit, by adding "\", then the result is wrong

  func testCallAnyExpressionSymbolEvaluatorConstant() {
        let expression = AnyExpression("foo('foo\\s', 'bar')", constants: [
            "foo": { ($0[0] as! String) + ($0[1] as! String) } as AnyExpression.SymbolEvaluator,
        ])
        XCTAssertEqual(try expression.evaluate(), "foo\\sbar")
        XCTAssertEqual(expression.symbols, [.function("foo", arity: 2)])
    }

I tested several test case, and noticed that if the parameter is string, and it contains \\, the result will be wrong.

Do you have any ideas to update this one? I think it relates to the function parseEscapedIdentifier.

Expose AST

Not a super pressing need, but it would be kind of fun if the AST was exposed (I believe subexpression is fileprivate right now). We're using this library for procedurally generating textures, so we need all the performance we can get. We were thinking of writing a little compiler that will compile the expression to Swift code, or maybe to shader code. Thanks for the library!

Question: Can multiplication be done with parentheses?

I am currently working on creating a calculator and have been using this framework for expression parsing, which has been going wonderfully so far. I am wondering if there is a way to implement multiplication with parentheses (for example "4(2)" = 8). If this is not possible with the current state of the framework, is it something worth adding in?

Ability to use ^ instead of pow(x, y)

I believe that using ^ as an alternate operator for powers would be a very beneficial addition. For example, instead of typing pow(4, 2), one could type 4^2 instead.

Fact `!` postfix operator does not work

let expression = "5!"

let result = try Expression(
    expression,
    symbols: [
        .postfix("!"): { Double(fact(Int($0[0]))) }
    ]
).evaluate()

print(result)

private func fact(_ number: Int) -> Int {
   if number == 0 {
      return 1
   } else {
      return number * fact(number - 1)
   }
}

I would expect 120, but the infix operator isn't called.

Getting sub expressions during evaluation

Problem:
I want to add a postfix % operator that takes in the last valid expression and transform it, however (and I'm still learning this library) it seems without a lot of parsing it's kind of challenging -- is there a way to transform something like this

10 + 2% to equal 10 + (10*2/100)

or

20 * 2 + 10% would give access to the last evaluated expression of (20 * 2) and the main argument 10 when getting the post fix callback for %

when using the sample code it won't execute

the sample code cut from the description:
var bar = 7 // variable
let expression = Expression("bas + baz(5) + rnd()", symbols: [
.variable("bar"): { _ in bar },
.function("baz", arity: 1): { args in args[0] as! Int + 1 },
.function("rnd", arity: 0): { _ in Double(arc4random()) }
])

Has this issue: "Type of expression is ambiguous without more context" "Expression" is imported

Usage for macOS Apps (Swift 5.1/Xcode 11.2.1)

Hi there,

this might be kinda awkward but I'm totally stuck here.

I wanted to try Expression for a little macOS App written in Swift 5 and Xcode 11. I have tried both just copying Expression.swift or installing as a framework by using cocoapods.

What am I doing wrong when Xcode keeps displaying

No such module 'Expression' ?

I have a totally naked project so no complex stuff in here - just a new project.

I saw in the Example folder that these were made for iOS (import UIKit, import Expression) and it works fine but there must be a(n awkward) thing I'm missing.

Sorry for this one. Just a beginner trying his best :-/

Equality of strings, substrings and characters

Intuitively I'd expect 'Test'[0] == 'T' to return true but it evaluates to false.

In equalArgs this lands in this case:

            case let (lhs as AnyHashable, rhs as AnyHashable):
                return lhs == rhs

This is due to rhs is a String while lhs is a Character.

This could be fixed by adding:

            case let (lhs as _String, rhs as _String):
                return lhs.substring == rhs.substring

However, this makes me wonder if all the other AnyHashable cases should also get _Substring variants. Alternatively, the _String cases in arrayEvaluator could be changed to always return a String, but that could lead to inefficiencies.

Any suggestions on how best to address this? Or does this work as expected?

Order of operations question

First off let me say I am really impressed with this library. I do have a question though. I am trying to include "^" as an alternative infix operator to the pow() function. Do you have any recommendation on how one could designate the order of operation for new math operators?

This is how I added the operator:

        Expression(
         Expression.parse(stringValue),
            impureSymbols: { symbol in
                switch symbol {
                case .infix("^"): return {  args in
                    return pow(args[0], args[1])
                }
                default: return nil
                }
            }
        )

Question: Is implicit multiplication possible with Expression?

Is implicit multiplication possible with Expression? I see according to #39 that implicit multiplication is sort of possible using a custom operator for parenthesis, but I haven't seen any indication that implicit multiplication without parenthesis is possible.

For example: try Expression("2 * pi") is valid, but try Expression("2pi") is not.

Using the recommendation in #39, I could add the custom operator for parenthesis, but in my case (a scientific calculator that takes user input), I'd have to parse the input beforehand and intelligently wrap anything that should be implicitly multiplied in parenthesis before passing it to Expression.

So is there any other way to enable implicit multiplication, or will I have to use the recommendation in #39 along with parsing the input separately to intelligently wrap certain parts of the expression in parenthesis?

problem with swift build, missing 0.3.0 tag ?

Hi,
If I'm trying to point to your repo, instead of my fork
.Package(url: "https://github.com/nicklockwood/Expression", Version(0,3,0))])
and run
$ swift build

Cloning https://github.com/nicklockwood/Expression
error: No version tag found in (/Users/franck/dev/mrboom_pi/asm2c/Packages/Expression) package. Add a version tag with "git tag" command. Example: "git tag 0.1.0"

I get this error, I think it's because you're missing a tag "0.3.0"
If I try to use a 0.3 tag instead:

    .Package(url: "https://github.com/nicklockwood/Expression", Version(0,3))])

I get this error: ^
Can't parse Package.swift manifest file because it contains invalid format. Fix Package.swift file format and try again.

Could you please try to add a "0.3.0" tag ?

variables with index

It would be great if we could support "myarray[1]" as a symbol. If you don't think its a good idea for the main branch could you offer a pointer to the best way to implement it anyway? :-)
thanks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.