diff --git a/Sources/jsonlogic/Parser.swift b/Sources/jsonlogic/Parser.swift index 30b514d..fd2d139 100644 --- a/Sources/jsonlogic/Parser.swift +++ b/Sources/jsonlogic/Parser.swift @@ -159,6 +159,64 @@ struct Comparison: Expression { } } +struct Round: Expression { + let arg: Expression + + func evalWithData(_ data: JSON?) throws -> JSON { + let result = try arg.evalWithData(data) + switch result { + case let .Array(array) where array.count == 2: + guard let numberToRound = array[0].double, + let numberOfPlaces = array[1].int + else { fallthrough } + + let divisor = pow(10.0, Double(numberOfPlaces)) + let result = (numberToRound * divisor).rounded() / divisor + return .Double(result) + default: + return result.toNumber() + } + } +} + +struct CastToNumber: Expression { + let arg: Expression + + func evalWithData(_ data: JSON?) throws -> JSON { + let result = try arg.evalWithData(data) + + switch result { + case let .Array(array): + let stringArray = array.compactMap { element -> String? in + switch element { + case .String(let value): + return value + default: + return nil + } + } + + guard stringArray.isEmpty == false else { return .Null } + + let doubleArray = stringArray.compactMap(Double.init).map(JSON.Double) + + guard doubleArray.isEmpty == false else { return .Null } + + if doubleArray.count == 1 { + return doubleArray[0] + } + + return .Array(doubleArray) + case let .String(string): + guard let value = Double(string) else { return .Null } + + return .Double(value) + default: + return .Null + } + } +} + //swiftlint:disable:next type_name struct If: Expression { let arg: ArrayOfExpressions @@ -216,6 +274,92 @@ struct Not: Expression { } } +struct Sin: Expression { + let arg: Expression + + func evalWithData(_ data: JSON?) throws -> JSON { + let jsonValue = try arg.evalWithData(data) + + if case let JSON.Array(array) = try arg.evalWithData(data) { + guard array.count > 1 else { + return array.first?.double.flatMap(sin).flatMap(JSON.Double) ?? + array.first?.int.flatMap(Double.init).flatMap(sin).flatMap(JSON.Double) ?? + JSON.Null + } + + return JSON.Array(array.compactMap { value in + return value.double.flatMap(sin).flatMap(JSON.Double) ?? + value.int.flatMap(Double.init).flatMap(sin).flatMap(JSON.Double) + }) + } else if case let JSON.Int(number) = jsonValue { + let sinValue = sin(Double(number)) + return JSON.Double(sinValue) + } else if case let JSON.Double(number) = jsonValue { + return JSON.Double(sin(number)) + } else { + return JSON.Null + } + } +} + +struct Cos: Expression { + let arg: Expression + + func evalWithData(_ data: JSON?) throws -> JSON { + let jsonValue = try arg.evalWithData(data) + + if case let JSON.Array(array) = try arg.evalWithData(data) { + guard array.count > 1 else { + return array.first?.double.flatMap(cos).flatMap(JSON.Double) ?? + array.first?.int.flatMap(Double.init).flatMap(cos).flatMap(JSON.Double) ?? + JSON.Null + } + + return JSON.Array(array.compactMap { value in + return value.double.flatMap(cos).flatMap(JSON.Double) ?? + value.int.flatMap(Double.init).flatMap(cos).flatMap(JSON.Double) + }) + } else if case let JSON.Int(number) = jsonValue { + let sinValue = cos(Double(number)) + return JSON.Double(sinValue) + } else if case let JSON.Double(number) = jsonValue { + return JSON.Double(cos(number)) + } else { + return JSON.Null + } + } + +} + +struct Tan: Expression { + let arg: Expression + + func evalWithData(_ data: JSON?) throws -> JSON { + let jsonValue = try arg.evalWithData(data) + + if case let JSON.Array(array) = try arg.evalWithData(data) { + guard array.count > 1 else { + return array.first?.double.flatMap(tan).flatMap(JSON.Double) ?? + array.first?.int.flatMap(Double.init).flatMap(tan).flatMap(JSON.Double) ?? + JSON.Null + } + + return JSON.Array(array.compactMap { value in + return value.double.flatMap(tan).flatMap(JSON.Double) ?? + value.int.flatMap(Double.init).flatMap(tan).flatMap(JSON.Double) + }) + } else if case let JSON.Int(number) = jsonValue { + let sinValue = tan(Double(number)) + return JSON.Double(sinValue) + } else if case let JSON.Double(number) = jsonValue { + return JSON.Double(tan(number)) + } else { + return JSON.Null + } + } + +} + struct Max: Expression { let arg: Expression @@ -660,6 +804,10 @@ class Parser { return Comparison(arg: try self.parse(json: value), operation: >=) case "<=": return Comparison(arg: try self.parse(json: value), operation: <=) + case "rnd": + return Round(arg: try self.parse(json: value)) + case "num": + return CastToNumber(arg: try self.parse(json: value)) case "if", "?:": guard let array = try self.parse(json: value) as? ArrayOfExpressions else { throw ParseError.GenericError("\(key) statement be followed by an array") @@ -676,6 +824,12 @@ class Parser { return Max(arg: try self.parse(json: value)) case "min": return Min(arg: try self.parse(json: value)) + case "sin": + return Sin(arg: try self.parse(json: value)) + case "cos": + return Cos(arg: try self.parse(json: value)) + case "tan": + return Tan(arg: try self.parse(json: value)) case "substr": guard let array = try self.parse(json: value) as? ArrayOfExpressions else { throw ParseError.GenericError("\(key) statement be followed by an array") diff --git a/Tests/jsonlogicTests/ArithmeticTests.swift b/Tests/jsonlogicTests/ArithmeticTests.swift index e537994..8273a44 100644 --- a/Tests/jsonlogicTests/ArithmeticTests.swift +++ b/Tests/jsonlogicTests/ArithmeticTests.swift @@ -167,4 +167,82 @@ class Arithmetic: XCTestCase { """ XCTAssertEqual(2, try applyRule(rule, to: nil)) } + + func testSin() { + var rule = + """ + { "sin": [\(Double.pi / 2)] } + """ + + XCTAssertEqual(1, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "sin": [\(Double.pi / 6)] } + """ + + XCTAssertEqual(0.5, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "sin": [\(Double.pi)] } + """ + + XCTAssertEqual(0, try applyRule(rule, to: nil), accuracy: 0.002) + } + + func testCos() { + var rule = + """ + { "cos": [\(Double.pi / 2)] } + """ + + XCTAssertEqual(0, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "cos": [\(Double.pi / 3)] } + """ + + XCTAssertEqual(0.5, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "cos": [\(2 * Double.pi)] } + """ + + XCTAssertEqual(1, try applyRule(rule, to: nil), accuracy: 0.002) + } + + func testTan() { + var rule = + """ + { "tan": [0] } + """ + + XCTAssertEqual(0, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "tan": [\(Double.pi / 3)] } + """ + + XCTAssertEqual(sqrt(3), try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "tan": [\(Double.pi / 4)] } + """ + + XCTAssertEqual(1, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "tan": [\(Double.pi / 2)] } + """ + + // In mathematics, this would be undefined + // In a programming language using IEEE 754, this is "a very large number" + XCTAssertGreaterThan(try applyRule(rule, to: nil), 1.5e16) + } } diff --git a/Tests/jsonlogicTests/NumberCastTests.swift b/Tests/jsonlogicTests/NumberCastTests.swift new file mode 100644 index 0000000..a728839 --- /dev/null +++ b/Tests/jsonlogicTests/NumberCastTests.swift @@ -0,0 +1,82 @@ +// +// NumberCastTests.swift +// jsonlogicTests +// +// Created by Dino on 22/07/2019. +// + +import XCTest +import JSON +@testable import jsonlogic + +class NumberCastTests: XCTestCase { + + func testCastNumericalString() { + var rule = + """ + { "num": ["0"] } + """ + + XCTAssertEqual(0, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "num": ["0.145"] } + """ + + XCTAssertEqual(0.145, try applyRule(rule, to: nil), accuracy: 0.002) + + rule = + """ + { "num": [".145"] } + """ + + XCTAssertEqual(0.145, try applyRule(rule, to: nil), accuracy: 0.002) + + + rule = + """ + { "num": ["3.14159"] } + """ + + XCTAssertEqual(Double.pi, try applyRule(rule, to: nil), accuracy: 0.002) + } + + func testCastInvalidString() { + var rule = + """ + { "num": ["Hello World"] } + """ + + XCTAssertThrowsError(try applyRule(rule, to: nil) as Optional) + + rule = + """ + { "num": ["0.14AF"] } + """ + + XCTAssertThrowsError(try applyRule(rule, to: nil) as Optional) + + rule = + """ + { "num": ["F0.14"] } + """ + + XCTAssertThrowsError(try applyRule(rule, to: nil) as Optional) + + rule = + """ + { "num": ["0...14"] } + """ + + XCTAssertThrowsError(try applyRule(rule, to: nil) as Optional) + + rule = + """ + { "num": ["2.1.4"] } + """ + + XCTAssertThrowsError(try applyRule(rule, to: nil) as Optional) + } + +} diff --git a/Tests/jsonlogicTests/NumericalOperations/RoundingTests.swift b/Tests/jsonlogicTests/NumericalOperations/RoundingTests.swift new file mode 100644 index 0000000..9080346 --- /dev/null +++ b/Tests/jsonlogicTests/NumericalOperations/RoundingTests.swift @@ -0,0 +1,102 @@ +// +// RoundingTests.swift +// jsonlogicTests +// +// Created by Dino on 22/07/2019. +// + +import XCTest +@testable import jsonlogic + +class RoundingTests: XCTestCase { + + func testRounding_noDecimals() { + var rule = + """ + { "rnd": [1.234, 0] } + """ + + XCTAssertEqual(1.0, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [1.789, 0] } + """ + + XCTAssertEqual(2.0, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [-0.7, 0] } + """ + + XCTAssertEqual(-1.0, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [-0.4, 0] } + """ + + XCTAssertEqual(0.0, try applyRule(rule, to: nil)) + } + + func testRounding_oneDecimal() { + var rule = + """ + { "rnd": [1.234, 1] } + """ + + XCTAssertEqual(1.2, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [1.789, 1] } + """ + + XCTAssertEqual(1.8, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [-0.7, 1] } + """ + + XCTAssertEqual(-0.7, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [-0.445, 1] } + """ + + XCTAssertEqual(-0.4, try applyRule(rule, to: nil)) + } + + func testRounding_twoDecimals() { + var rule = + """ + { "rnd": [1.234, 2] } + """ + + XCTAssertEqual(1.23, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [1.789, 2] } + """ + + XCTAssertEqual(1.79, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [-0.799, 1] } + """ + + XCTAssertEqual(-0.80, try applyRule(rule, to: nil)) + + rule = + """ + { "rnd": [-0.495, 1] } + """ + + XCTAssertEqual(-0.50, try applyRule(rule, to: nil)) + } +} diff --git a/jsonlogic.podspec b/jsonlogic.podspec index 6f61700..2ea8420 100644 --- a/jsonlogic.podspec +++ b/jsonlogic.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "jsonlogic" - s.version = "1.0.0" + s.version = "1.1.0" s.summary = "A JsonLogic Swift library" s.description = "A JsonLogic implementation in Swift. JsonLogic is a way to write rules that involve computations in JSON format, these can be applied on JSON data with consistent results. So you can share between server and clients rules in a common format." s.homepage = "https://github.com/advantagefse/json-logic-swift"