Can I force NSExpression and expressionValue to assume Doubles instead of Ints somehow?

坚强是说给别人听的谎言 提交于 2020-01-09 08:11:33

问题


I'm trying to do math from a string.

When I turn a string into a math problem with NSExpression, and then get the result with expressionValue, Swift assumes I want an Integer. Consider these two Playground examples:

let currentCalculation = "10 / 6"
let currentExpression = NSExpression(format: currentCalculation)
print(currentExpression) // 10 / 6
if let result = currentExpression.expressionValue(with: nil, context: nil) as? Double {
    print(result) // 1
}

let anotherCalculation = "10.0 / 6.0"
let anotherExpression = NSExpression(format: anotherCalculation)
print(anotherExpression) // 10 / 6
if let result = anotherExpression.expressionValue(with: nil, context: nil) as? Double {
    print(result) // 1.666666667
}

What should I be doing so that I always get a Double as a result? I don't want to have to parse the string ahead of time.

Pretty interesting that the second example turns "anotherExpression" into Integers, yet still returns a Double as a result.


回答1:


You might be better off using a 3rd party expression parser/evaluator, such as DDMathParser. NSExpression is quite limited, and has no options to force floating point evaluation.

If you want to (or have to) stick to NSExpression: Here is a possible solution to (recursively) replace all constant values in an expression by their floating point value:

extension NSExpression {

    func toFloatingPoint() -> NSExpression {
        switch expressionType {
        case .constantValue:
            if let value = constantValue as? NSNumber {
                return NSExpression(forConstantValue: NSNumber(value: value.doubleValue))
            }
        case .function:
           let newArgs = arguments.map { $0.map { $0.toFloatingPoint() } }
           return NSExpression(forFunction: operand, selectorName: function, arguments: newArgs)
        case .conditional:
           return NSExpression(forConditional: predicate, trueExpression: self.true.toFloatingPoint(), falseExpression: self.false.toFloatingPoint())
        case .unionSet:
            return NSExpression(forUnionSet: left.toFloatingPoint(), with: right.toFloatingPoint())
        case .intersectSet:
            return NSExpression(forIntersectSet: left.toFloatingPoint(), with: right.toFloatingPoint())
        case .minusSet:
            return NSExpression(forMinusSet: left.toFloatingPoint(), with: right.toFloatingPoint())
        case .subquery:
            if let subQuery = collection as? NSExpression {
                return NSExpression(forSubquery: subQuery.toFloatingPoint(), usingIteratorVariable: variable, predicate: predicate)
            }
        case .aggregate:
            if let subExpressions = collection as? [NSExpression] {
                return NSExpression(forAggregate: subExpressions.map { $0.toFloatingPoint() })
            }
        case .anyKey:
            fatalError("anyKey not yet implemented")
        case .block:
            fatalError("block not yet implemented")
        case .evaluatedObject, .variable, .keyPath:
            break // Nothing to do here
        }
        return self
    }
}

Example:

let expression = NSExpression(format: "10/6+3/4")
if let result = expression.toFloatingPoint().expressionValue(with: nil, context: nil) as? Double {
    print("result:", result) // 2.41666666666667
}

This works with "simple" expressions using arithmetic operators and functions and some "advanced" expression types (unions, intersections, ...). The remaining conversions can be added if necessary.




回答2:


Here's a variant of Martin R's great answer that has two important changes:

  • It only converts the arguments to division. Any other functions can still receive integral arguments.
  • It handles expressions like count({1,2,3,4,5}) / count({1,2}) where the arguments to division aren't constant values.

Code:

import Foundation

extension NSExpression {
    func toFloatingPointDivision() -> NSExpression {
        switch expressionType {
        case .function where function == "divide:by:":
            guard let args = arguments else { break }
            let newArgs = args.map({ arg -> NSExpression in
                if arg.expressionType == .constantValue {
                    if let value = arg.constantValue as? Double {
                        return NSExpression(forConstantValue: value)
                    } else {
                        return arg
                    }
                } else {
                    return NSExpression(block: { (object, arguments, context) in
                        // NB: The type of `+[NSExpression expressionForBlock:arguments]` is incorrect.
                        // It claims the arguments is an array of NSExpressions, but it's not, it's
                        // actually an array of the evaluated values. We can work around this by going
                        // through NSArray.
                        guard let arg = (arguments as NSArray).firstObject else { return NSNull() }
                        return (arg as? Double) ?? arg
                    }, arguments: [arg.toFloatingPointDivision()])
                }
            })
            return NSExpression(forFunction: operand, selectorName: function, arguments: newArgs)
        case .function:
            guard let args = arguments else { break }
            let newArgs = args.map({ $0.toFloatingPointDivision() })
            return NSExpression(forFunction: operand, selectorName: function, arguments: newArgs)
        case .conditional:
            return NSExpression(forConditional: predicate,
                                trueExpression: self.true.toFloatingPointDivision(),
                                falseExpression: self.false.toFloatingPointDivision())
        case .unionSet:
            return NSExpression(forUnionSet: left.toFloatingPointDivision(), with: right.toFloatingPointDivision())
        case .intersectSet:
            return NSExpression(forIntersectSet: left.toFloatingPointDivision(), with: right.toFloatingPointDivision())
        case .minusSet:
            return NSExpression(forMinusSet: left.toFloatingPointDivision(), with: right.toFloatingPointDivision())
        case .subquery:
            if let subQuery = collection as? NSExpression {
                return NSExpression(forSubquery: subQuery.toFloatingPointDivision(), usingIteratorVariable: variable, predicate: predicate)
            }
        case .aggregate:
            if let subExpressions = collection as? [NSExpression] {
                return NSExpression(forAggregate: subExpressions.map({ $0.toFloatingPointDivision() }))
            }
        case .block:
            guard let args = arguments else { break }
            let newArgs = args.map({ $0.toFloatingPointDivision() })
            return NSExpression(block: expressionBlock, arguments: newArgs)
        case .constantValue, .anyKey:
        break // Nothing to do here
        case .evaluatedObject, .variable, .keyPath:
            // FIXME: These should probably be wrapped in blocks like the one
            // used in the `.function` case.
            break
        }
        return self
    }
}


来源:https://stackoverflow.com/questions/46550658/can-i-force-nsexpression-and-expressionvalue-to-assume-doubles-instead-of-ints-s

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!