// // ExpressionTests.swift // ExpressionTests // // Created by Nick Lockwood on 15/09/2016. // Copyright © 2016 Nick Lockwood. All rights reserved. // // Distributed under the permissive MIT license // Get the latest version from here: // // https://github.com/nicklockwood/Expression // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // import XCTest @testable import Expression final class ExpressionTests: XCTestCase { func testLinuxTestSuiteIncludesAllTests() { #if os(macOS) let thisClass = type(of: self) let linuxCount = thisClass.__allTests.count let darwinCount = thisClass.defaultTestSuite.testCaseCount XCTAssertEqual(linuxCount, darwinCount, "run swift test --generate-linuxmain") #endif } // MARK: Description func testDescriptionSpacing() { let expression = Expression("a+b") XCTAssertEqual(expression.description, "a + b") } func testDescriptionParensAdded() { let expression = Expression("a+b*c") XCTAssertEqual(expression.description, "a + b * c") } func testDescriptionParensPreserved() { let expression = Expression("a*(b+c)") XCTAssertEqual(expression.description, "a * (b + c)") } func testDescriptionParensPreserved2() { let expression = Expression("(a+b)*c") XCTAssertEqual(expression.description, "(a + b) * c") } func testDescriptionRedundantParensDiscarded() { let expression = Expression("(a+b)+c") XCTAssertEqual(expression.description, "a + b + c") } func testIntExpressionDescription() { let expression = Expression("32 + 200014") XCTAssertEqual(expression.description, "200046") } func testFloatExpressionDescription() { let expression = Expression("2.4 + 7.65") XCTAssertEqual(expression.description, "10.05") } func testPrefixOperatorDescription() { let expression = Expression("-foo") XCTAssertEqual(expression.description, "-foo") } func testPrefixOperatorInsidePostfixExpressionDescription() { let expression = Expression("(-foo)%") XCTAssertEqual(expression.description, "-foo%") } func testInfixOperatorInsidePrefixExpressionDescription() { let expression = Expression("-(a+b)") XCTAssertEqual(expression.description, "-(a + b)") } func testPrefixRangeFollowedByLessThan() { let expression = Expression("...(<5)") XCTAssertEqual(expression.description, "...(<5)") } func testNestedPrefixOperatorDescription() { let expression = Expression("- -foo") XCTAssertEqual(expression.description, "-(-foo)") } func testPostfixOperatorDescription() { let expression = Expression("foo%") XCTAssertEqual(expression.description, "foo%") } func testNestedPostfixOperatorDescription() { let expression = Expression("foo% !") XCTAssertEqual(expression.description, "(foo%)!") } func testPostfixOperatorInsidePrefixExpressionDescription() { let expression = Expression("-(foo%)") XCTAssertEqual(expression.description, "-(foo%)") } func testInfixOperatorInsidePostfixExpressionDescription() { let expression = Expression("(a+b)%") XCTAssertEqual(expression.description, "(a + b)%") } func testPostfixOperatorInsideInfixExpressionDescription() { let expression = Expression("foo% + 5") XCTAssertEqual(expression.description, "foo% + 5") } func testPostfixAlphanumericOperatorDescription() { let expression = Expression("5ms") XCTAssertEqual(expression.description, "5ms") } func testPostfixAlphanumericOperatorDescription2() { let expression = Expression("foo ms") XCTAssertEqual(expression.description, "(foo)ms") } func testPostfixAlphanumericOperatorInsidePrefixExpressionDescription() { let expression = Expression("-foo ms") XCTAssertEqual(expression.description, "(-foo)ms") } func testInfixAlphanumericOperatorDescription() { let expression = Expression("foo or bar") XCTAssertEqual(expression.description, "foo or bar") } func testNestedPostfixAlphanumericOperatorsDescription() { let expression = Expression("(foo bar) baz") XCTAssertEqual(expression.description, "((foo)bar)baz") } func testRightAssociativeOperatorsDescription() { let expression = Expression("(a = b) = c") XCTAssertEqual(expression.description, "(a = b) = c") } func testRightAssociativeOperatorsDescription2() { let expression = Expression("a == b > c") XCTAssertEqual(expression.description, "a == b > c") } func testInfixDotOperatorDescription() { let expression = Expression("(foo).(bar)") XCTAssertEqual(expression.description, "foo . bar") } func testPrefixDotOperatorDescription() { let expression = Expression(".(foo)") XCTAssertEqual(expression.description, ".(foo)") } func testCommaSpacingDescription() { let expression = Expression("a,b") XCTAssertEqual(expression.description, "a, b") } func testCommaSpacingAfterOperatorDescription() { let expression = Expression("a % , b") XCTAssertEqual(expression.description, "a%, b") } func testErrorDescription() { let expression = Expression("0x") XCTAssertEqual(expression.description, "0x") } func testEscapedVariableDescription() { let expression = Expression.parse("`hello\\tworld`") XCTAssertEqual(expression.description, "`hello\\tworld`") } func testEscapedFunctionDescription() { let expression = Expression.parse("`hello\\tworld`(x)") XCTAssertEqual(expression.description, "`hello\\tworld`(x)") } func testEscapedPostfixOperatorDescription() { let expression = Expression.parse("5`metric\\ttonnes`") XCTAssertEqual(expression.description, "5`metric\\ttonnes`") } func testEscapedPostfixOperatorChainDescription() { let expression = Expression.parse("(5foo)`bar\\tbaz`") XCTAssertEqual(expression.description, "(5foo)`bar\\tbaz`") } func testSumOfTuplesDescription() { let expression = Expression.parse("(3,4) + (5,6)") XCTAssertEqual(expression.description, "(3, 4) + (5, 6)") } func testFunctionWithTupleArgumentDescription() { let expression = Expression.parse("foo((5,6))") XCTAssertEqual(expression.description, "foo((5, 6))") } func testFunctionWithMultipleTupleArgumentsDescription() { let expression = Expression.parse("foo((3,4),(5,6))") XCTAssertEqual(expression.description, "foo((3, 4), (5, 6))") } func testArrayWithTupleArgumentDescription() { let expression = Expression.parse("foo[(5,6)]") XCTAssertEqual(expression.description, "foo[(5, 6)]") } func testArrayLiteralDescription() { let expression = Expression.parse("[1,2,3]") XCTAssertEqual(expression.description, "[1, 2, 3]") } func testArrayOperatorDescription() { let expression = Expression.parse("3 * 3[4] + 2") XCTAssertEqual(expression.description, "3 * 3[4] + 2") } func testArrayOperatorDescription2() { let expression = Expression.parse("3 * 3[4 * 2] + 3") XCTAssertEqual(expression.description, "3 * 3[4 * 2] + 3") } // MARK: Error description func testCustomErrorDescription() { let error = Expression.Error.message("foo") XCTAssertEqual(error.description, "foo") } func testEmptyExpressionErrorDescription() { let error = Expression.Error.emptyExpression XCTAssertEqual(error.description, "Empty expression") } func testUnexpectedTokenErrorDescription() { let error = Expression.Error.unexpectedToken(")") XCTAssertEqual(error.description, "Unexpected token `)`") } func testMissingDelimiterErrorDescription() { let error = Expression.Error.missingDelimiter("]") XCTAssertEqual(error.description, "Missing `]`") } func testMissingUndefinedSymbolErrorDescription() { let error = Expression.Error.undefinedSymbol(.postfix("foo")) XCTAssertEqual(error.description, "Undefined postfix operator foo") } func testArrayArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.array("foo")) XCTAssertEqual(error.description, "Array foo[] expects 1 argument") } func testZeroFunctionArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.function("foo", arity: 0)) XCTAssertEqual(error.description, "Function foo() expects 0 arguments") } func testUnaryFunctionArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.function("foo", arity: 1)) XCTAssertEqual(error.description, "Function foo() expects 1 argument") } func testBinaryFunctionArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.function("foo", arity: 2)) XCTAssertEqual(error.description, "Function foo() expects 2 arguments") } func testVariadicFunctionArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.function("foo", arity: .atLeast(1))) XCTAssertEqual(error.description, "Function foo() expects at least 1 argument") } func testVariadicFunctionArityMismatchErrorDescription2() { let error = Expression.Error.arityMismatch(.function("foo", arity: .atLeast(2))) XCTAssertEqual(error.description, "Function foo() expects at least 2 arguments") } func testInfixOperatorArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.infix("foo")) XCTAssertEqual(error.description, "Infix operator foo expects 2 arguments") } func testTernaryOperatorArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.infix("?:")) XCTAssertEqual(error.description, "Ternary operator ?: expects 3 arguments") } func testSubscriptOperatorArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.infix("[]")) XCTAssertEqual(error.description, "Subscript operator [] expects 1 argument") } func testFunctionCallOperatorArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.infix("()")) XCTAssertEqual(error.description, "Function call operator () expects at least 1 argument") } func testPostfixOperatorArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.postfix("foo")) XCTAssertEqual(error.description, "Postfix operator foo expects 1 argument") } func testPrefixOperatorArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.prefix("foo")) XCTAssertEqual(error.description, "Prefix operator foo expects 1 argument") } func testVariableArityMismatchErrorDescription() { let error = Expression.Error.arityMismatch(.variable("foo")) XCTAssertEqual(error.description, "Variable foo expects 0 arguments") } func testEmptySymbolArityErrorDescription() { // TODO: is this the correct behavior? let error = Expression.Error.arityMismatch(.variable("")) XCTAssertEqual(error.description, "Variable expects 0 arguments") } func testArrayBoundsErrorDescription() { let error = Expression.Error.arrayBounds(.array("foo"), 5) XCTAssertEqual(error.description, "Index 5 out of bounds for array foo[]") } // MARK: Error equatability func testMessageErrorEquality() { let error = Expression.Error.message("foo") XCTAssertEqual(error, error) XCTAssertNotEqual(error, .message("bar")) XCTAssertNotEqual(error, .unexpectedToken("foo")) } func testUnexpectedTokenErrorEquality() { let error = Expression.Error.unexpectedToken(")") XCTAssertEqual(error, error) XCTAssertNotEqual(error, .unexpectedToken("]")) XCTAssertNotEqual(error, .missingDelimiter(")")) } func testMissingDelimiterErrorEquality() { let error = Expression.Error.missingDelimiter("]") XCTAssertEqual(error, error) XCTAssertNotEqual(error, .missingDelimiter(")")) XCTAssertNotEqual(error, .unexpectedToken("]")) } func testUndefinedSymbolErrorEquality() { let error = Expression.Error.undefinedSymbol(.array("foo")) XCTAssertEqual(error, error) XCTAssertNotEqual(error, .undefinedSymbol(.array("bar"))) XCTAssertNotEqual(error, .undefinedSymbol(.variable("foo"))) XCTAssertNotEqual(error, .arityMismatch(.array("foo"))) } func testArityMismatchErrorEquality() { let error = Expression.Error.arityMismatch(.function("foo", arity: 1)) XCTAssertEqual(error, error) XCTAssertNotEqual(error, .arityMismatch(.function("foo", arity: 2))) XCTAssertNotEqual(error, .arityMismatch(.function("bar", arity: 1))) XCTAssertNotEqual(error, .arityMismatch(.array("foo"))) XCTAssertNotEqual(error, .undefinedSymbol(.array("bar"))) } func testArrayBoundsErrorEquality() { let error = Expression.Error.arrayBounds(.array("foo"), 2) XCTAssertNotEqual(error, .arrayBounds(.array("foo"), 3)) XCTAssertNotEqual(error, .arrayBounds(.array("bar"), 2)) XCTAssertNotEqual(error, .arityMismatch(.array("foo"))) } // MARK: Numbers func testZero() { let expression = Expression("0") XCTAssertEqual(try expression.evaluate(), 0) } func testSmallInteger() { let expression = Expression("5") XCTAssertEqual(try expression.evaluate(), 5) } func testLargeInteger() { let expression = Expression("12345678901234567890") XCTAssertEqual(try expression.evaluate(), 12345678901234567890) } func testNegativeInteger() { let expression = Expression("-7") XCTAssertEqual(try expression.evaluate(), -7) } func testSmallFloat() { let expression = Expression("0.2") XCTAssertEqual(try expression.evaluate(), 0.2) } func testLargeFloat() { let expression = Expression("1234.567890") XCTAssertEqual(try expression.evaluate(), 1234.567890) } func testNegativeFloat() { let expression = Expression("-0.34") XCTAssertEqual(try expression.evaluate(), -0.34) } func testLeadingDecimalPoint() { let expression = Expression(".5") XCTAssertEqual(try expression.evaluate(), 0.5) } func testExponential() { let expression = Expression("1234e5") XCTAssertEqual(try expression.evaluate(), 1234e5) } func testPositiveExponential() { let expression = Expression("0.123e+4") XCTAssertEqual(try expression.evaluate(), 0.123e+4) } func testNegativeExponential() { let expression = Expression("0.123e-4") XCTAssertEqual(try expression.evaluate(), 0.123e-4) } func testCapitalExponential() { let expression = Expression("0.123E-4") XCTAssertEqual(try expression.evaluate(), 0.123e-4) } func testInvalidExponential() { let expression = Expression("123.e5") XCTAssertThrowsError(try expression.evaluate()) } func testLeadingZeros() { let expression = Expression("0005") XCTAssertEqual(try expression.evaluate(), 0005) } func testHex() { let expression = Expression("0x2A ") XCTAssertEqual(try expression.evaluate(), 0x2A) } // MARK: Quoted identifiers (strings) func testDoubleQuotedIdentifier() { let expression = Expression.parse("\"foo\" + \"bar\"") XCTAssertEqual(expression.symbols, [.variable("\"foo\""), .infix("+"), .variable("\"bar\"")]) } func testSingleQuotedIdentifier() { let expression = Expression.parse("'foo' + 'bar'") XCTAssertEqual(expression.symbols, [.variable("'foo'"), .infix("+"), .variable("'bar'")]) } func testTrailingSingleQuotes() { let expression = Expression.parse("foo' + bar'") XCTAssertEqual(expression.symbols, [.variable("foo'"), .infix("+"), .variable("bar'")]) } func testBacktickEscapedIdentifier() { let expression = Expression.parse("`foo` + `bar`") XCTAssertEqual(expression.symbols, [.variable("`foo`"), .infix("+"), .variable("`bar`")]) } func testBacktickEscapedIdentifierWithEscapedChars() { let expression = Expression.parse("`foo\\`bar\\n`") XCTAssertEqual(expression.symbols, [.variable("`foo`bar\n`")]) } func testValidateQuotedIdentifierContainingNull() { let expression = Expression.parse("'foo\\0bar'") XCTAssertEqual(expression.symbols, [.variable("'foo\0bar'")]) XCTAssertEqual(expression.description, "'foo\\0bar'") } func testValidateQuotedIdentifierContainingTab() { let expression = Expression.parse("'foo\\tbar'") XCTAssertEqual(expression.symbols, [.variable("'foo\tbar'")]) XCTAssertEqual(expression.description, "'foo\\tbar'") } func testValidateQuotedIdentifierContainingNewline() { let expression = Expression.parse("'foo\\nbar'") XCTAssertEqual(expression.symbols, [.variable("'foo\nbar'")]) XCTAssertEqual(expression.description, "'foo\\nbar'") } func testValidateQuotedIdentifierContainingCarriageReturn() { let expression = Expression.parse("'foo\\rbar'") XCTAssertEqual(expression.symbols, [.variable("'foo\rbar'")]) XCTAssertEqual(expression.description, "'foo\\rbar'") } func testValidateQuotedIdentifierContainingDelete() { let expression = Expression.parse("'foo\\u{7F}bar'") XCTAssertEqual(expression.symbols, [.variable("'foo\u{7F}bar'")]) XCTAssertEqual(expression.description, "'foo\\u{7F}bar'") } func testValidateQuotedIdentifierContainingUnitSeparator() { let expression = Expression.parse("'foo\\u{1F}bar'") XCTAssertEqual(expression.symbols, [.variable("'foo\u{1F}bar'")]) XCTAssertEqual(expression.description, "'foo\\u{1F}bar'") } func testValidateQuotedIdentifierContainingEmoji() { let expression = Expression.parse("'foo🤡bar'") XCTAssertEqual(expression.symbols, [.variable("'foo🤡bar'")]) XCTAssertEqual(expression.description, "'foo🤡bar'") } // MARK: Ambiguous whitespace func testPostfixOperatorAsInfix() { let expression = Expression.parse("a+ b", usingCache: false) XCTAssertEqual(expression.description, "a + b") } func testPostfixOperatorAsInfix2() { let expression = Expression.parse("1+ 2", usingCache: false) XCTAssertEqual(expression.description, "1 + 2") } func testPostfixOperatorAsInfix3() { let expression = Expression.parse("a+ (b)", usingCache: false) XCTAssertEqual(expression.description, "a + b") } func testParenthesizedPostfixOperator() { let expression = Expression.parse("(a +) b", usingCache: false) XCTAssertEqual(expression.description, "(a+)b") } func testParenthesizedPostfixOperator2() { let expression = Expression.parse("(a +) +", usingCache: false) XCTAssertEqual(expression.description, "(a+)+") } func testParenthesizedPrefixOperator() { let expression = Expression.parse("+ (+ a)", usingCache: false) XCTAssertEqual(expression.description, "+(+a)") } func testPrefixOperatorAsInfix() { let expression = Expression.parse("a +b", usingCache: false) XCTAssertEqual(expression.description, "a + b") } func testPrefixOperatorAsInfix2() { let expression = Expression.parse("1 +2", usingCache: false) XCTAssertEqual(expression.description, "1 + 2") } func testSpaceBeforePrefixOperator() { let expression = Expression.parse(" -1", usingCache: false) XCTAssertEqual(expression.description, "-1") } func testSpaceAroundPrefixOperator() { let expression = Expression.parse(" - 1", usingCache: false) XCTAssertEqual(expression.description, "-1") } func testSpaceBeforeInfixExpression() { let expression = Expression.parse(" 1 + 2", usingCache: false) XCTAssertEqual(expression.description, "1 + 2") } func testPlusFollowedByDecimalPoint() { let expression = Expression.parse("1+.5", usingCache: false) XCTAssertEqual(expression.description, "1 + 0.5") } func testDotsFollowedByLessThan() { let expression = Expression.parse("1..<5", usingCache: false) XCTAssertEqual(expression.description, "1 ..< 5") } func testLiteralPlusNegativeLiteral() { let expression = Expression("5+-4", options: .noOptimize) XCTAssertEqual(expression.description, "5 + -4") } func testLiteralPercentNegativeLiteral() { let expression = Expression("5%-4") XCTAssertEqual(expression.description, "5% - 4") } // MARK: Delimited expressions func testBracedExpression() { let input = "{ 1 + 2 }" var characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening { var expression = Expression.parse(&characters) XCTAssertEqual(characters.first, "}") guard expression.error == .unexpectedToken("}") else { XCTFail() return } characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening { expression = Expression.parse(&characters, upTo: "\"") XCTAssertEqual(characters.first, "}") guard expression.error == .unexpectedToken("}") else { XCTFail() return } characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening { expression = Expression.parse(&characters, upTo: "}") XCTAssertEqual(characters.first, "}") XCTAssertEqual(expression.description, "1 + 2") XCTAssertNil(expression.error) } func testQuotedExpression() { let input = "\" 1 + 2 \"" var characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening quote var expression = Expression.parse(&characters) XCTAssertNil(characters.first) guard expression.error == .unexpectedToken("\"") else { XCTFail() return } characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening quote expression = Expression.parse(&characters, upTo: "}") XCTAssertNil(characters.first) guard expression.error == .unexpectedToken("\"") else { XCTFail() return } characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening quote expression = Expression.parse(&characters, upTo: "\"") XCTAssertEqual(characters.first, "\"") XCTAssertEqual(expression.description, "1 + 2") XCTAssertNil(expression.error) } func testEmptyBracedExpression() { let input = "{}" var characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening { let expression = Expression.parse(&characters, upTo: "}") XCTAssertEqual(characters.first, "}") XCTAssertEqual(expression.error, .emptyExpression) } func testAllWhitespaceBracedExpression() { let input = "{ \n }" var characters = Substring.UnicodeScalarView(input.unicodeScalars) characters.removeFirst() // Remove opening { let expression = Expression.parse(&characters, upTo: "}") XCTAssertEqual(characters.first, "}") XCTAssertEqual(expression.error, .emptyExpression) } // MARK: Syntax errors func testMissingCloseParen() { let expression = Expression("(1 + (2 + 3)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter(")")) } } func testMissingOpenParen() { let expression = Expression("1 + 2)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken(")")) } } func testMissingClosingFunctionParen() { let expression = Expression("foo(") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter(")")) } } func testMissingRHS() { let expression = Expression("1 + ") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.postfix("+"))) } } func testTrailingDot() { let expression = Expression("foo.", constants: ["foo": 5]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.postfix("."))) } } func testTrailingDotFollowedBySpace() { let expression = Expression("foo. ", constants: ["foo": 5]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.postfix("."))) } } func testTrailingDecimalPoint() { let expression = Expression("5.") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.postfix("."))) } } func testInvalidExpression() { let expression = Expression("0 5") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("5")) } } func testEmptyExpression() { let expression = Expression("") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .emptyExpression) XCTAssertEqual(error as? Expression.Error, .unexpectedToken("")) } } func testStandaloneOperator() { let expression = Expression("+") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("+")) } } func testUnterminatedString() { let expression = Expression("'foo") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter("'")) } } func testUnterminatedStringEscapeSequence() { let expression = Expression("'foo\\") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter("'")) } } func testUnterminatedStringAfterEscapeSequence() { let expression = Expression("'foo\\'") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter("'")) } } func testUnicodeLiteralMissingNumber() { let expression = Expression("'foo\\u{}") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("}")) } } func testUnicodeLiteralMissingNumberAndBrace() { let expression = Expression("'foo\\u{") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter("}")) } } func testUnicodeLiteralContainsJunk() { let expression = Expression("'foo\\u{bar}'") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("r}'")) } } func testUnicodeLiteralMissingClosingBrace() { let expression = Expression("'foo\\u{5") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter("}")) } } func testInvalidUnicodeCodepoint() { let expression = Expression("'foo\\u{DDDD}'") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("DDDD")) } } func testMissingClosingBracket() { let expression = Expression("foo[0", arrays: ["foo": [1]]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter("]")) } } func testMissingIndexExpression() { let expression = Expression("foo[", arrays: ["foo": [1]]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .missingDelimiter("]")) } } func testEmptyBrackets() { let expression = Expression("foo[]", arrays: ["foo": []]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.array("foo"))) } } func testTrailingParen() { let expression = Expression("5)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken(")")) } } func testTrailingParenThenSpace() { let expression = Expression("5) ") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken(")")) } } func testTrailingWhitespace() { let expression = Expression("5\n") XCTAssertNoThrow(try expression.evaluate()) } func testTrailingE() { let expression = Expression("5e") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.postfix("e"))) } } func testTrailingEPlus() { let expression = Expression("5e+", symbols: [.postfix("e"): { _ in 1 }]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.postfix("+"))) } } func testTrailingHexPrefix() { let expression = Expression("0x") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("0x")) } } func testTrailingHexPrefixAfterPrefixOperator() { let expression = Expression("-0x") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("0x")) } } func testTrailingHexPrefixAfterInfxOperator() { let expression = Expression("5 + 0x") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("0x")) } } func testMissingFirstArgument() { let expression = Expression.parse("foo(,5)") XCTAssertEqual(expression.error, .unexpectedToken(",")) } func testMissingLastArgument() { // TODO: should trailing commas be allowed? let expression = Expression.parse("foo(1,)") XCTAssertEqual(expression.error, .unexpectedToken(")")) } func testMissingMiddleArgument() { let expression = Expression.parse("foo(1,,2)") XCTAssertEqual(expression.error, .unexpectedToken(",")) } // MARK: Arity errors func testTooFewArguments() { let expression = Expression("pow(4)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.function("pow", arity: 2))) } } func testTooFewArgumentsForCustomFunction() { let expression = Expression("foo(4)", symbols: [ .function("foo", arity: 2): { $0[0] + $0[1] }, ]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.function("foo", arity: 2))) } } func testTooFewArgumentsWithAdvancedInitializer() { let expression = Expression(Expression.parse("pow(4)"), pureSymbols: { _ in nil }) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.function("pow", arity: 2))) } } func testTooFewArgumentsForCustomFunctionWithAdvancedInitializer() { let expression = Expression(Expression.parse("foo(4)"), pureSymbols: { symbol in switch symbol { case .function("foo", arity: 2): return { $0[0] + $0[1] } default: return nil } }) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.function("foo", arity: 2))) } } func testTooManyArguments() { let expression = Expression("pow(4,5,6)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.function("pow", arity: 2))) } } func testTooManyArgumentsWithAdvancedInitializer() { let expression = Expression(Expression.parse("pow(4,5,6)"), impureSymbols: { _ in nil }) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.function("pow", arity: 2))) } } func testTooFewVariadicArguments() { let expression = Expression("min(3)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.function("min", arity: .atLeast(2)))) } } // MARK: Symbol errors func testUnknownSymbolError() { let expression = Expression(Expression.parse("foo()")) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.function("foo", arity: 0))) } } func testUnknownSymbolWithAdvancedInitializer() { let expression = Expression(Expression.parse("foo()"), pureSymbols: { _ in nil }) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.function("foo", arity: 0))) } } // MARK: Function overloading func testOverridePow() { let expression = Expression("pow(3)", symbols: [.function("pow", arity: 1): { $0[0] * $0[0] }]) XCTAssertEqual(try expression.evaluate(), 9) } func testOverriddenPow() { let expression = Expression("pow(3,3)", symbols: [.function("pow", arity: 1): { $0[0] * $0[0] }]) XCTAssertEqual(try expression.evaluate(), 27) } func testCustomOverriddenFunction() { let expression = Expression("foo(3,3)", symbols: [ .function("foo", arity: 1): { args in args[0] }, .function("foo", arity: 2): { args in args[0] + args[1] }, ]) XCTAssertEqual(try expression.evaluate(), 6) } // MARK: Function blocking func testDisablePow() { let symbol = Expression.Symbol.function("pow", arity: 2) let expression = Expression("pow(1,2)", symbols: [symbol: { _ in throw Expression.Error.undefinedSymbol(symbol) }]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .undefinedSymbol(.function("pow", arity: 2))) } } // MARK: Function chaining func testCallNumericLiteral() { let expression = Expression("1(3)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("(")) } } func testCallNumericLiteralWithFunctionCallOperator() { let expression = Expression("1(3)", symbols: [ .infix("()"): { $0[0] + $0[1] }, ]) XCTAssertEqual(try expression.evaluate(), 4) } func testCallResultOfFunction() { let expression = Expression("pow(1,2)(3)") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("(")) } } func testCallResultOfFunctionWithFunctionCallOperator() { let expression = Expression("pow(1,2)(3)", symbols: [ .infix("()"): { $0[0] + $0[1] }, ]) XCTAssertEqual(try expression.evaluate(), 4) } func testCallResultOfSubscript() { let expression = Expression("foo[1](3)", symbols: [ .array("foo"): { _ in 1 }, ]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("(")) } } func testCallResultOfSubscriptWithFunctionCallOperator() { let expression = Expression("foo[1](3)", symbols: [ .array("foo"): { _ in 1 }, .infix("()"): { $0[0] + $0[1] }, ]) XCTAssertEqual(try expression.evaluate(), 4) } func testCallArrayLiteral() { let expression = Expression("[1,2](3)", symbols: [ .function("[]", arity: .any): { _ in 1 }, ]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("(")) } } func testCallArrayLiteralWithFunctionCallOperator() { let expression = Expression("[1,2](3)", symbols: [ .function("[]", arity: .any): { _ in 1 }, .infix("()"): { $0[0] + $0[1] }, ]) XCTAssertEqual(try expression.evaluate(), 4) } // MARK: Arrays func testSubscriptConstantArray() { let expression = Expression("foo[2]", arrays: ["foo": [1, 2, 3]]) XCTAssertEqual(try expression.evaluate(), 3) } func testArrayBoundsError() { let expression = Expression("foo[3]", arrays: ["foo": [1, 2, 3]]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arrayBounds(.array("foo"), 3)) } } func testSubscriptCustomArray() { let expression = Expression("foo[2]", symbols: [.array("foo"): { args in [1, 2, 3][Int(args[0])] }]) XCTAssertEqual(try expression.evaluate(), 3) } func testMultiArgSubscript() { let expression = Expression("foo[2,3]", symbols: [ .array("foo"): { _ in 0 }, ]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.array("foo"))) } } func testUndefinedTupleSubscript() { let expression = Expression("foo[(2,3)]", symbols: [ .array("foo"): { _ in 0 }, ]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken(",")) } } func testSubscriptResultOfSubscript() { let expression = Expression("foo[2][3]", symbols: [.array("foo"): { _ in 0 }]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("[")) } } func testSubscriptResultOfFunction() { let expression = Expression("pow(2,3)[2]") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("[")) } } func testUndefinedArrayLiteral() { let expression = Expression("[1,2,3]") XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("[")) } } func testArrayLiteral() { let expression = Expression("[1,2,3]", symbols: [ .function("[]", arity: .any): { $0.reduce(0) { $0 + $1 } }, ]) XCTAssertEqual(try expression.evaluate(), 6) } func testAddArrayLiteral() { let expression = Expression("[1,2,3] + [4,5,6]", symbols: [ .function("[]", arity: .any): { $0.reduce(0) { $0 + $1 } }, ]) XCTAssertEqual(try expression.evaluate(), 21) } func testSubscriptArrayLiteral() { let expression = Expression("[1,2][3]", symbols: [ .function("[]", arity: .any): { $0.reduce(0) { $0 + $1 } }, ]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .unexpectedToken("[")) } } func testSubscriptArrayLiteralWithCustomSubscriptOperator() { let expression = Expression("[1,2][0]", symbols: [ .function("[]", arity: .any): { $0.reduce(0) { $0 + $1 } }, .infix("[]"): { args in let digits = Array(String(Int(args[0]))) let index = Int(args[1]) if index >= digits.count { return .nan } return Double(String(digits[index])) ?? 0 }, ]) XCTAssertEqual(try expression.evaluate(), 3) } func testSubscriptNumericLiteralWithCustomSubscriptOperator() { let expression = Expression("534[2]", symbols: [ .infix("[]"): { args in let digits = Array(String(Int(args[0]))) let index = Int(args[1]) if index >= digits.count { return .nan } return Double(String(digits[index])) ?? 0 }, ]) XCTAssertEqual(try expression.evaluate(), 4) } func testSubscriptNumericLiteralWithCustomSubscriptOperatorWithMultipleArguments() { let expression = Expression("534[2,4]", symbols: [ .infix("[]"): { _ in 5 }, ]) XCTAssertThrowsError(try expression.evaluate()) { error in XCTAssertEqual(error as? Expression.Error, .arityMismatch(.infix("[]"))) } } // MARK: Evaluation func testLiteral() { let expression = Expression("5") XCTAssertEqual(try expression.evaluate(), 5) } func testNegativeLiteral() { let expression = Expression("- 12") XCTAssertEqual(try expression.evaluate(), -12) } func testVariable() { let expression = Expression("foo", constants: ["foo": 15.5]) XCTAssertEqual(try expression.evaluate(), 15.5) } func testNegativeVariable() { let expression = Expression("-foo", constants: ["foo": 7]) XCTAssertEqual(try expression.evaluate(), -7) } func testLiteralAddition() { let expression = Expression("5 + 4") XCTAssertEqual(try expression.evaluate(), 9) } func testLiteralPlusVariable() { let expression = Expression("3 + foo", constants: ["foo": -7]) XCTAssertEqual(try expression.evaluate(), -4) } func testTwoAdditions() { let expression = Expression("5 + foo + 4", constants: ["foo": 1.5]) XCTAssertEqual(try expression.evaluate(), 10.5) } func testAdditionThenMultiplication() { let expression = Expression("5 + foo * 4", constants: ["foo": 1.5]) XCTAssertEqual(try expression.evaluate(), 11) } func testAdditionThenMultiplicationWithPrefixMinus() { let expression = Expression("5 + foo * -4", constants: ["foo": 1.5]) XCTAssertEqual(try expression.evaluate(), -1) } func testMultiplicationThenAddition() { let expression = Expression("5 * foo + 4", constants: ["foo": 1.5]) XCTAssertEqual(try expression.evaluate(), 11.5) } func testParenthesizedAdditionThenMultiplication() { let expression = Expression("(5 + foo) * 4", constants: ["foo": 1.5]) XCTAssertEqual(try expression.evaluate(), 26) } func testNestedParenthese() { let expression = Expression("((5 + 3) * ((2 - 3) - 1))") XCTAssertEqual(try expression.evaluate(), -16) } func testModFunction() { let expression = Expression("mod(-4, 2.5)") XCTAssertEqual(try expression.evaluate(), -1.5) } func testSqrtFunction() { let expression = Expression("7 + sqrt(9)") XCTAssertEqual(try expression.evaluate(), 10) } func testPowFunction() { let expression = Expression("7 + pow(9, 1/2)") XCTAssertEqual(try expression.evaluate(), 10) } func testVariadicMinFunction() { let expression = Expression("min(3, 2, 7)") XCTAssertEqual(try expression.evaluate(), 2) } func testVariadicMaxFunction() { let expression = Expression("max(7, 8, 9)") XCTAssertEqual(try expression.evaluate(), 9) } // MARK: Function parsing func testParseEmptyFunction() { let expression = Expression.parse("foo()") XCTAssertEqual(expression.symbols, [.function("foo", arity: 0)]) XCTAssertEqual(expression.description, "foo()") } func testParseEmptyFunctionContainingSpace() { let expression = Expression.parse("foo( )") XCTAssertEqual(expression.symbols, [.function("foo", arity: 0)]) XCTAssertEqual(expression.description, "foo()") } func testParseFunctionWithOneArgumentAndSpaces() { let expression = Expression.parse("foo( 5 )") XCTAssertEqual(expression.symbols, [.function("foo", arity: 1)]) XCTAssertEqual(expression.description, "foo(5)") } func testParseFunctionWithTwoArgumentsAndSpaces() { let expression = Expression.parse("foo( 5 , 6 )") XCTAssertEqual(expression.symbols, [.function("foo", arity: 2)]) XCTAssertEqual(expression.description, "foo(5, 6)") } func testParseFunctionWithNestedParensAndSpaces() { let expression = Expression.parse("foo( (( (5)) ))") XCTAssertEqual(expression.symbols, [.function("foo", arity: 1)]) XCTAssertEqual(expression.description, "foo(5)") } func testParseFunctionWithTupleArgument() { let expression = Expression.parse("foo((5,6))") XCTAssertEqual(expression.symbols, [.infix(","), .function("foo", arity: 1)]) XCTAssertEqual(expression.description, "foo((5, 6))") } // MARK: Postfix operator parsing func testPostfixOperatorBeforeComma() { let expression = Expression("max(50%, 0.6)", symbols: [ .postfix("%"): { args in args[0] / 100 }, ]) XCTAssertEqual(try expression.evaluate(), 0.6) } func testPostfixOperatorBeforeClosingParen() { let expression = Expression("min(0.3, 50%)", symbols: [ .postfix("%"): { args in args[0] / 100 }, ]) XCTAssertEqual(try expression.evaluate(), 0.3) } func testWronglySpacedPostfixOperator() { let expression = Expression("50 % + 10%", symbols: [ .postfix("%"): { args in args[0] / 100 }, ]) XCTAssertEqual(try expression.evaluate(), 0.6) } // MARK: Alphanumeric operators func testPostfixAlphanumericOperator() { let expression = Expression("10ms + 5s", symbols: [ .postfix("ms"): { args in args[0] / 1000 }, .postfix("s"): { args in args[0] }, ]) XCTAssertEqual(try expression.evaluate(), 5.01) } func testInfixAlphanumericOperator() { let expression = Expression("true or false", options: .boolSymbols, symbols: [ .infix("or"): { args in if args[0] != 0 { return 1 } if args[1] != 0 { return 1 } return 0 }, ]) XCTAssertEqual(try expression.evaluate(), 1) } // MARK: Math errors func testDivideByZero() { let expression = Expression("1 / 0") XCTAssertEqual(try expression.evaluate(), Double.infinity) } func testHugeNumber() { let expression = Expression("19911919912912919291291291921929123") XCTAssertEqual(try expression.evaluate(), 19911919912912919291291291921929123) } // MARK: Symbols func testModExpressionSymbols() { let expression = Expression("mod(foo, bar)", symbols: [ .variable("foo"): { _ in 5 }, .variable("bar"): { _ in 2.5 }, ]) XCTAssertEqual(expression.symbols, [.function("mod", arity: 2), .variable("foo"), .variable("bar")]) } func testPrefixSymbol() { let expression = Expression("-foo", symbols: [.variable("foo"): { _ in 5 }]) let expected: Set = [.prefix("-"), .variable("foo")] XCTAssertEqual(expression.symbols, expected) } func testPostfixSymbol() { let expression = Expression("foo++", symbols: [ .variable("foo"): { _ in 5 }, .postfix("++"): { args in args[0] + 1 }, ]) let expected: Set = [.postfix("++"), .variable("foo")] XCTAssertEqual(expression.symbols, expected) } // MARK: Optimization func testConstantSymbolsInlined() { let expression = Expression("foo(bar, baz)", constants: ["bar": 5, "baz": 2.5]) XCTAssertEqual(expression.symbols, [.function("foo", arity: 2)]) XCTAssertEqual(expression.description, "foo(5, 2.5)") } func testConstantSymbolsInlined2() { let expression = Expression("foo[bar]", constants: ["bar": 0]) XCTAssertEqual(expression.symbols, [.array("foo")]) XCTAssertEqual(expression.description, "foo[0]") } func testConstantExpressionEvaluatedCorrectly() { let expression = Expression("5 + foo", constants: ["foo": 5]) XCTAssertEqual(try expression.evaluate(), 10) } func testConstantInlined() { let expression = Expression("5 + foo", constants: ["foo": 5]) XCTAssertEqual(expression.symbols, []) XCTAssertEqual(expression.description, "10") } func testConstantInlined2() { let expression = Expression("5 + foo", constants: ["foo": 5], symbols: [.variable("bar"): { _ in 6 }]) XCTAssertEqual(expression.symbols, []) XCTAssertEqual(expression.description, "10") } func testVariableSymbolNotInlined() { let expression = Expression("5 + foo", options: .pureSymbols, symbols: [.variable("foo"): { _ in 5 }]) XCTAssertEqual(expression.symbols, [.variable("foo"), .infix("+")]) XCTAssertEqual(expression.description, "5 + foo") } func testArrayInlined() { let expression = Expression("5 + foo[0]", arrays: ["foo": [5]]) XCTAssertEqual(expression.symbols, []) XCTAssertEqual(expression.description, "10") } func testArraySymbolNotInlined() { let expression = Expression("5 + foo[0]", options: .pureSymbols, symbols: [.array("foo"): { _ in 5 }]) XCTAssertEqual(expression.symbols, [.array("foo"), .infix("+")]) XCTAssertEqual(expression.description, "5 + foo[0]") } func testPotentiallyImpureConstantNotInlined() { let expression = Expression("5 + foo", symbols: [.variable("foo"): { _ in 5 }]) XCTAssertEqual(expression.symbols, [.variable("foo"), .infix("+")]) XCTAssertEqual(expression.description, "5 + foo") } func testPotentiallyImpureArrayNotInlined() { let expression = Expression("5 + foo[0]", symbols: [.array("foo"): { _ in 5 }]) XCTAssertEqual(expression.symbols, [.array("foo"), .infix("+")]) XCTAssertEqual(expression.description, "5 + foo[0]") } func testPureExpressionInlined() { let expression = Expression("min(5, 6) + a") XCTAssertEqual(expression.symbols, [.variable("a"), .infix("+")]) XCTAssertEqual(expression.description, "5 + a") } func testPotentiallyImpureExpressionNotInlined() { let expression = Expression("min(5, 6) + a", symbols: [.function("min", arity: 2): { min($0[0], $0[1]) }]) XCTAssertEqual(expression.symbols, [.function("min", arity: 2), .variable("a"), .infix("+")]) XCTAssertEqual(expression.description, "min(5, 6) + a") } func testBooleanExpressionsInlined() { let expression = Expression("1 || 1 ? 3 * 5 : 2 * 3", options: .boolSymbols) XCTAssertEqual(expression.symbols, []) XCTAssertEqual(expression.description, "15") } func testVariableDoesntBreakOptimizer() { let expression = Expression( "foo ? bar : baz", options: .boolSymbols, constants: ["bar": 5, "baz": 6], symbols: [.variable("foo"): { _ in 1 }] ) XCTAssertEqual(expression.symbols, [.variable("foo"), .infix("?:")]) XCTAssertEqual(expression.description, "foo ? 5 : 6") } func testOptimizerDisabled() { let expression = Expression("3 * 5", options: .noOptimize) XCTAssertEqual(expression.symbols, [.infix("*")]) XCTAssertEqual(expression.description, "3 * 5") } // MARK: Pure symbols func testOverriddenBuiltInConstantNotInlined() { let expression = Expression("5 + pi", symbols: [.variable("pi"): { _ in 5 }]) XCTAssertEqual(expression.symbols, [.infix("+"), .variable("pi")]) XCTAssertEqual(expression.description, "5 + pi") } func testOverriddenBuiltInConstantNotInlinedWithPureSymbols() { let expression = Expression("5 + pi", options: .pureSymbols, symbols: [.variable("pi"): { _ in 5 }]) XCTAssertEqual(expression.symbols, [.infix("+"), .variable("pi")]) XCTAssertEqual(expression.description, "5 + pi") } func testOverriddenBuiltInFunctionNotInlined() { let expression = Expression("5 + floor(1.5)", symbols: [ .function("floor", arity: 1): { args in ceil(args[0]) }, ]) XCTAssertEqual(expression.symbols, [.infix("+"), .function("floor", arity: 1)]) XCTAssertEqual(expression.description, "5 + floor(1.5)") } func testOverriddenBuiltInFunctionInlinedWithPureSymbols() { let expression = Expression("5 + floor(1.5)", options: .pureSymbols, symbols: [ .function("floor", arity: 1): { args in ceil(args[0]) }, ]) XCTAssertEqual(expression.symbols, []) XCTAssertEqual(expression.description, "7") } func testCustomFunctionNotInlined() { let expression = Expression("5 + foo()", symbols: [.function("foo", arity: 0): { _ in 5 }]) XCTAssertEqual(expression.symbols, [.infix("+"), .function("foo", arity: 0)]) XCTAssertEqual(expression.description, "5 + foo()") } func testCustomFunctionInlinedWithPureSymbols() { let expression = Expression("5 + foo()", options: .pureSymbols, symbols: [.function("foo", arity: 0): { _ in 5 }]) XCTAssertEqual(expression.symbols, []) XCTAssertEqual(expression.description, "10") } // MARK: Dot operators func testDotInsideIdentifier() { let expression = Expression("foo.bar", options: .boolSymbols) XCTAssertEqual(expression.symbols, [.variable("foo.bar")]) } func testDotBetweenParens() { let expression = Expression("(foo).(bar)", options: .boolSymbols) XCTAssertEqual(expression.symbols, [.variable("foo"), .infix("."), .variable("bar")]) } func testDotFollowedByNumber() { let expression = Expression("foo.3", options: .boolSymbols) XCTAssertEqual(expression.symbols, [.variable("foo.3")]) } func testIdentifierWithLeadingDot() { let expression = Expression(".foo", options: .boolSymbols) XCTAssertEqual(expression.symbols, [.variable(".foo")]) } func testRangeOperator() { let expression = Expression("foo..bar", options: .boolSymbols) XCTAssertEqual(expression.symbols, [.variable("foo"), .infix(".."), .variable("bar")]) } // MARK: Ternary operator func testTernaryTrue() { let expression = Expression("0 ? 1 : 2", options: .boolSymbols) XCTAssertEqual(try expression.evaluate(), 2) } func testTernaryFalse() { let expression = Expression("1 ? 1 : 2", options: .boolSymbols) XCTAssertEqual(try expression.evaluate(), 1) } func testTernaryPrecedence() { let expression = Expression("1 - 1 ? 3 * 5 : 2 * 3", options: .boolSymbols) XCTAssertEqual(try expression.evaluate(), 6) } func testUndefinedTernaryOperator() { let symbols: [Expression.Symbol: Expression.SymbolEvaluator] = [ .infix("?"): { $0[0] != 0 ? $0[1] : 0 }, .infix(":"): { $0[0] != 0 ? $0[0] : $0[1] }, ] let expression = Expression("1 - 1 ? 3 * 5 : 2 * 3", symbols: symbols) XCTAssertEqual(expression.description, "0 ? 15 : 6") XCTAssertEqual(try expression.evaluate(), 6) } func testTernaryWith2Arguments() { let expression1 = Expression("5 ?: 4", options: .boolSymbols) XCTAssertEqual(try expression1.evaluate(), 5) let expression2 = Expression("0 ?: 4", options: .boolSymbols) XCTAssertEqual(try expression2.evaluate(), 4) } // MARK: Modulo operator func testPostiveIntegerModulo() { let expression = Expression("5 % 2") XCTAssertEqual(try expression.evaluate(), 1) } func testNegativeIntegerModulo() { let expression = Expression("5 % -2") XCTAssertEqual(try expression.evaluate(), 1) } func testNegativeIntegerModulo2() { let expression = Expression("-5 % 2") XCTAssertEqual(try expression.evaluate(), -1) } func testNegativeIntegerModulo3() { let expression = Expression("-5 % -2") XCTAssertEqual(try expression.evaluate(), -1) } func testPostiveFloatModulo() { let expression = Expression("5.5 % 2") XCTAssertEqual(try expression.evaluate(), 1.5) } func testNegativeFloatModulo() { let expression = Expression("5.5 % -2") XCTAssertEqual(try expression.evaluate(), 1.5) } // MARK: Assignment func testAssignmentAssociativity() { var variables: [Double] = [0, 0] let expression = Expression("a = b = 5", symbols: [ .infix("="): { args in variables[Int(args[0])] = args[1] return args[1] }, .variable("a"): { _ in 0 }, .variable("b"): { _ in 1 }, ]) XCTAssertEqual(try expression.evaluate(), 5) XCTAssertEqual(variables[0], 5) XCTAssertEqual(variables[1], 5) } // MARK: Identifier validation func testValidateSimpleIdentifier() { XCTAssertTrue(Expression.isValidIdentifier("foo")) } func testValidateIdentifierWithTrailingSpace() { XCTAssertFalse(Expression.isValidIdentifier("foo ")) } func testValidateIdentifierWithLeadingSpace() { XCTAssertFalse(Expression.isValidIdentifier(" foo")) } func testValidateUnicodeIdentifier() { XCTAssertTrue(Expression.isValidIdentifier("🤡")) } func testValidateDotDelimitedIdentifier() { XCTAssertTrue(Expression.isValidIdentifier("foo.bar")) } func testValidateDotPrefixedIdentifier() { XCTAssertTrue(Expression.isValidIdentifier(".foo")) } func testValidateDotSuffixedIdentifier() { XCTAssertFalse(Expression.isValidIdentifier("foo.")) } func testValidateIdentifierWithTrailingApostrophe() { XCTAssertTrue(Expression.isValidIdentifier("x'")) } func testValidateIdentifierWithTrailingDotApostrophe() { XCTAssertFalse(Expression.isValidIdentifier("x.'")) } func testValidateIdentifierWithLeadingApostrophe() { XCTAssertFalse(Expression.isValidIdentifier("'x")) } func testValidateEscapedIdentifier() { XCTAssertTrue(Expression.isValidIdentifier("`foo bar`")) } func testValidateQuotedIdentifier() { XCTAssertTrue(Expression.isValidIdentifier("'foo bar'")) } func testValidateDoubleQuotedIdentifier() { XCTAssertTrue(Expression.isValidIdentifier("\"foo 'bar'\"")) } func testValidateOperatorAsIdentifier() { XCTAssertFalse(Expression.isValidIdentifier("+")) } func testValidateEmptyIdentifier() { XCTAssertFalse(Expression.isValidIdentifier("")) } // MARK: Operator validation func testValidateSimpleOperator() { XCTAssertTrue(Expression.isValidOperator("+")) } func testValidateTripleEqualsOperator() { XCTAssertTrue(Expression.isValidOperator("===")) } func testValidateTernaryOperator() { XCTAssertTrue(Expression.isValidOperator("?:")) } func testValidateUnicodeOperator() { XCTAssertTrue(Expression.isValidOperator("•")) } func testValidateOpenParenAsOperator() { XCTAssertFalse(Expression.isValidOperator("(")) } func testValidateOpenBracketAsOperator() { XCTAssertFalse(Expression.isValidOperator("[")) } func testValidateCommaOperator() { XCTAssertTrue(Expression.isValidOperator(",")) } func testValidateCommaSequenceAsOperator() { XCTAssertFalse(Expression.isValidOperator(",,")) } func testValidateColonOperator() { XCTAssertTrue(Expression.isValidOperator(":")) } func testValidateColonSequenceAsOperator() { XCTAssertTrue(Expression.isValidOperator("::")) } func testValidateIdentifierAsOperator() { XCTAssertFalse(Expression.isValidOperator("foo")) } func testValidateEmptyOperator() { XCTAssertFalse(Expression.isValidOperator("")) } // MARK: Operator precedence func testEqualityIsRightAssociative() { let expression = Expression("a == b == c", options: .boolSymbols, constants: ["a": 1, "b": 2, "c": 2]) XCTAssertEqual(try expression.evaluate(), 1) } func testBitshiftTakesPrecedenceOverAddition() { let expression = Expression("1 + 2 << 3", symbols: [.infix("<<"): { Double(Int($0[0]) << Int($0[1])) }]) XCTAssertEqual(try expression.evaluate(), 17) } func testMultiplicationTakesPrecedenceOverAddition() { let expression = Expression("2 + 3 * 2") XCTAssertEqual(try expression.evaluate(), 8) } func testAdditionTakesPrecedenceOverRange() { let expression = Expression("1 ... 3 * 4", symbols: [.infix("..."): { $0[0] + $0[1] }]) XCTAssertEqual(try expression.evaluate(), 13) } func testRangeTakesPrecedenceOverIs() { let expression = Expression("3 is 1 ... 2", symbols: [ .infix("..."): { $0[0] + $0[1] }, .infix("is"): { $0[0] == $0[1] ? 1 : 0 }, ] as [Expression.Symbol: ([Double]) throws -> Double]) XCTAssertEqual(try expression.evaluate(), 1) } func testIsTakesPrecedenceOverTernary() { let expression = Expression("0 ? 1 is 2 : 2 is 2", options: .boolSymbols, symbols: [ .infix("is"): { $0[0] == $0[1] ? 1 : 0 }, ]) XCTAssertEqual(try expression.evaluate(), 1) } func testSubtractionTakesPrecedenceOverNullCoalescing() { let expression = Expression("1 - 1 ?? 2 + 1", symbols: [.infix("??"): { $0[0] != 0 ? $0[0] : $0[1] }]) XCTAssertEqual(try expression.evaluate(), 3) } func testEqualityTakesPrecedenceOverAssignment() { let expression = Expression("2 = 3 == 1", options: .boolSymbols, symbols: [.infix("="): { $0[1] }]) XCTAssertEqual(try expression.evaluate(), 0) } func testEqualityTakesPrecedenceOverAnd() { let expression = Expression("1 == 1 && 2 == 2", options: .boolSymbols) XCTAssertEqual(try expression.evaluate(), 1) } func testEverythingTakesPrecedenceOverComma() { let expression = Expression("3 * 2, 2 * 3", symbols: [.infix(","): { $0[0] + $0[1] }]) XCTAssertEqual(try expression.evaluate(), 12) } func testSubscriptTakesPrecedenceOverMultiplications() { let expression = Expression("2 * 3[3] * 4", symbols: [.infix("[]"): { $0[0] + $0[1] }]) XCTAssertEqual(try expression.evaluate(), 48) } func testSubscriptArgumentTakesPrecedence() { let expression = Expression("3[3 + 2] * 4", symbols: [.infix("[]"): { $0[0] + $0[1] }]) XCTAssertEqual(try expression.evaluate(), 32) } func testWhitespaceDoesNotaffectPrecedence() { let expression = Expression("3 * 4 +5") XCTAssertEqual(try expression.evaluate(), 17) } func testWhitespaceDoesNotaffectPrecedence2() { let expression = Expression("3 * 4+ 5") XCTAssertEqual(try expression.evaluate(), 17) } func testWhitespaceDoesNotaffectPrecedence3() { let expression = Expression("3 *4 + 5") XCTAssertEqual(try expression.evaluate(), 17) } func testWhitespaceDoesNotaffectPrecedence4() { let expression = Expression("3* 4 + 5") XCTAssertEqual(try expression.evaluate(), 17) } // MARK: Symbol precedence func testConstantTakesPrecedenceOverSymbol() { let expression = Expression( "foo", constants: ["foo": 5], symbols: [.variable("foo"): { _ in 6 }] ) XCTAssertEqual(try expression.evaluate(), 5) } func testArrayConstantTakesPrecedenceOverSymbol() { let expression = Expression( "foo[0]", arrays: ["foo": [5]], symbols: [.array("foo"): { _ in 6 }] ) XCTAssertEqual(try expression.evaluate(), 5) } // MARK: Cache func testCache() { let expression = "foo + 5" Expression.clearCache() XCTAssertFalse(Expression.isCached(expression)) _ = Expression(expression) XCTAssertTrue(Expression.isCached(expression)) Expression.clearCache(for: expression) XCTAssertFalse(Expression.isCached(expression)) } }