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

3

4

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.

Goldfarb answered 3/10, 2017 at 17:59 Comment(0)
D
5

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.

Debidebilitate answered 3/10, 2017 at 19:42 Comment(13)
Dammit I was just about to post this exact same thing! BTW you'll probably want to replace NSExpression(forFunction: function, arguments: newArgs) with NSExpression(forFunction: operand, selectorName: function, arguments: newArgs). Otherwise you'll break expressions that invoke a selector on a value.Tantalic
Also I don't see why the block version can't be converted. Just provide your own new block that calls the original and then converts the value to Double if possible.Tantalic
@KevinBallard: What I meant is that a block (which is code, not text) cannot be converted, but you are right (with both comments). – If you have a more complete solution, please post it and I'll delete mine.Debidebilitate
My solution was actually going to be less complete; I didn't bother with .conditional, only with .constantValue and .function as those are the two expression types found in 10 / 6.Tantalic
It occurs to me that converting integers to doubles really only matters for division, so you might want to restrict the .function to only converting arguments if the function == "divide:by:".Tantalic
Kevin: That might fail for nested expressions, but I‘ll check it tomorrow (too late for today here).Debidebilitate
Martin: Ah right, you'd have to change it a bit to have the .function case explicitly check each argument to see if it's a .constantValue, rather than having it simply recurse into each arg. It also occurs to me that this solution in general won't work for an expression like count({1,2,3,4,5}) / count({1,2}). The actual solution there is to wrap all arguments to the divide:by: function in another expression that converts integers to blocks.Tantalic
I went ahead and posted a new answer that handles expressions like count({1,2,3,4,5}) / count({1,2}).Tantalic
Hi, thank you! It sounds like you are saying: NSExpression interprets the string's contents as Int; the output remains an Int when I used .expressionvalue; there is no way to ask for a Double calculation. You recommend: add custom function to NSExpression. That custom function is a switch-case for "expressionType", with which I am unfamiliar, an attribute of any NSExpression. I am unfamiliar with the cases--they appear to represent types of expressions that might be inside my string and transforms them using floating point functions. Which case did your example trigger? ("10/6+3/4") Thanks!Goldfarb
@Beagley: My first suggestion is to use a different expression parser! For example, DDMathParser has no problems with division, and is open source. You can even add additional functions (e.g. trigonometric functions, which NSExpression does not know).Debidebilitate
@Beagley: NSExpression(format: ...) parses the string into a tree-like structure, where each node is an NSExpression. My code recursively transforms that tree into a new one where all "constant expression nodes" are replaced by a new node with a "double constant expression".Debidebilitate
Thanks Martin R! As I am doing this as a learning exercise, I'm reluctant to utilize a 3rd party library just yet. I figure I should be able to write my own parsing. When the math gets more complex than +-/*(), I may crack. Thanks for explaining the basics behind what NSExpression(format:) is doing. I'm guessing that the case triggered by basic math is ".function"... I don't think I know enough to understand "let newArgs = arguments.map { $0.map { $0.toFloatingPoint() } } return NSExpression(forFunction: operand, selectorName: function, arguments: newArgs)"... but I'm close. :-)Goldfarb
Hi, @MartinR. This looks great. I would like to use it in my (MIT-licensed) project, but AFAIK I can't use it as-is unless I rewrite it, because of licensing (it's CC-BY-SA unless otherwise noted). I know some people use a different license for their SO code—do you do that (or would you be so kind as to give a license for this snippet)? Thanks.Staphylococcus
T
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
    }
}
Tantalic answered 3/10, 2017 at 22:26 Comment(6)
No, only converting the arguments of the division does not work. Try it with "10/6+3/4", it will evaluate to 1+0=1 instead of 2.41666666666667. Your code "sees" only the addition and then does not descend into its operands.Debidebilitate
Oops you're right, I didn't recurse into the arguments of other functions. I still stand by my claim that we should only actually convert the arguments of division into doubles, but of course we still need to check other functions to find nested divisions.Tantalic
@MartinR I just updated my code to handle other functions. I'm not at a Mac right now so I can't actually test it though.Tantalic
There is still a problem with your approach. "(1/2)/(4/3)" evaluates to 0.0 instead of 0.375.Debidebilitate
Thanks so much for your help! I am reluctant to use your/Martin R's suggestions until I know enough code to understand them. (i.e. no cheating.) I'm not far enough along to walk through the particular cases and understand what is happening. In the meantime, I will work on the other side of the problem-- (Every time the user types, I can append the correct "version" of what they typed to both a String and an NSExpression, interpreting the input as it comes in to make certain that numbers become Doubles, etc. I'll use the String to display and the NSExpression for the calculation.Goldfarb
@MartinR Fixed. I was missing a single call to toFloatingPointDivision()Tantalic
D
0

Just use RegEx to convert all values to floats. Example code below:

(Note: If you are passing in variables via the expressionValueWithObject: argument, make sure those are all non-integer as well.)

NSString *equation = @"1/2";//your equation here

/*Convert all numbers to floats so integer-arithmetic doesn't occur*/ {
    
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[0-9.]+" options:NSRegularExpressionCaseInsensitive error:NULL];
    
    NSArray *matches = [regex matchesInString:equation options:0 range:NSMakeRange(0, equation.length)] ;
    
    int integerConversions = 0;
    for (NSTextCheckingResult *match in matches) {
        
        NSRange originalRange = match.range;
        NSRange adjustedRange = NSMakeRange(originalRange.location+(integerConversions*@".0".length), originalRange.length);
        NSString *value = [equation substringWithRange:adjustedRange];
        
        if ([value containsString:@"."]) {
            continue;
        } else {
            equation = [equation stringByReplacingCharactersInRange:adjustedRange withString:[NSString stringWithFormat:@"%@.0", value];
            integerConversions++;
        }
            
    }
        
}

I wrote this in objective-c but it works converted to swift as well.

Dancer answered 29/10, 2022 at 21:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.