我可以强制 NSExpression 和 expressionValue 以某种方式假设 Doubles 而不是 Ints 吗?

Posted

技术标签:

【中文标题】我可以强制 NSExpression 和 expressionValue 以某种方式假设 Doubles 而不是 Ints 吗?【英文标题】:Can I force NSExpression and expressionValue to assume Doubles instead of Ints somehow? 【发布时间】:2017-10-03 17:59:21 【问题描述】:

我正在尝试从字符串中进行数学运算。

当我用 NSExpression 把一个字符串变成一个数学问题,然后用 expressionValue 得到结果时,Swift 假设我想要一个整数。考虑以下两个 Playground 示例:

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

我应该怎么做才能总是得到一个 Double 结果?我不想提前解析字符串。

非常有趣的是,第二个示例将“anotherExpression”转换为整数,但结果仍然返回 Double。

【问题讨论】:

【参考方案1】:

使用第 3 方表达式解析器/评估器可能会更好, 如DDMathParser。 NSExpression 非常有限,没有强制浮动的选项 点评价。

如果您想(或必须)坚持使用NSExpression:这是(递归)替换所有常量的可能解决方案 表达式中的值按其浮点值:

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
    

例子:

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

这适用于使用算术运算符和函数的“简单”表达式以及一些“高级”表达式类型(联合、交集等)。如有必要,可以添加剩余的转化次数。

【讨论】:

该死的,我正要发布这个完全相同的东西!顺便说一句,您可能希望将NSExpression(forFunction: function, arguments: newArgs) 替换为NSExpression(forFunction: operand, selectorName: function, arguments: newArgs)。否则,您将破坏在值上调用选择器的表达式。 我也不明白为什么无法转换块版本。只需提供您自己的新块来调用原始块,然后尽可能将值转换为Double @KevinBallard:我的意思是不能转换一个块(它是代码,而不是文本),但你是对的(两个 cmets)。 – 如果您有更完整的解决方案,请发布它,我会删除我的。 我的解决方案实际上会不太完整;我没有打扰.conditional,只有.constantValue.function 因为这是10 / 6 中的两种表达式类型。 我突然想到,将整数转换为双精度实际上只对除法很重要,因此您可能希望将 .function 限制为仅在 function == "divide:by:" 时转换参数。【参考方案2】:

这是 Martin R 出色答案的变体,有两个重要变化:

它只将参数转换为除法。任何其他函数仍然可以接收整数参数。 它处理像 count(1,2,3,4,5) / count(1,2) 这样的表达式,其中除法的参数不是常量值。

代码:

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
    

【讨论】:

不行,只转换除法的参数是行不通的。尝试使用"10/6+3/4",它将评估为1+0=1 而不是2.41666666666667。您的代码仅“看到”加法,然后不会下降到其操作数。 哎呀,你是对的,我没有递归到其他函数的参数。我仍然坚持我的主张,即我们应该只将除法的参数实际转换为双精度数,但当然我们仍然需要检查其他函数以找到嵌套的除法。 @MartinR 我刚刚更新了我的代码来处理其他功能。我现在不在 Mac 上,所以我实际上无法测试它。 你的方法还是有问题。 "(1/2)/(4/3)" 计算结果为 0.0 而不是 0.375。 非常感谢您的帮助!在我知道足够的代码来理解它们之前,我不愿意使用你/Martin R 的建议。 (即没有作弊。)我还远远不够了解具体案例并了解正在发生的事情。与此同时,我将解决问题的另一面——(每次用户输入时,我都可以将他们输入的正确“版本”附加到 String 和 NSExpression 中,并在输入时解释输入以确保数字变为双精度数等。我将使用 String 来显示和 NSExpression 进行计算。

以上是关于我可以强制 NSExpression 和 expressionValue 以某种方式假设 Doubles 而不是 Ints 吗?的主要内容,如果未能解决你的问题,请参考以下文章

从 NSExpression 捕获 NSInvalidArgumentException

NSExpression 尊重子查询 NSPredicate

使用 NSExpression 时捕获 NSInvalidArgumentException 的正确方法 [重复]

NSExpression 1/2

如何使 NSExpression 的 expressionForFunction:withArguments: 尊重获取请求的谓词

NSExpression 捕获无效参数