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

后端 未结 2 1091
情歌与酒
情歌与酒 2020-12-07 03:58

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

相关标签:
2条回答
  • 2020-12-07 04:09

    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.

    0 讨论(0)
  • 2020-12-07 04:17

    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
        }
    }
    
    0 讨论(0)
提交回复
热议问题