Skip to content

Enhancement Request: Support decimal parameters/parsing #777

Open
@JosephGray

Description

@JosephGray

Please add support to use the System.Decimal type (my own use case only requires decimal for parameters but it would be nice to support for parsing as well). To prevent breaking changes to existing code that utilizes xFunc, the support can be made optional so that default behavior is to continue to use double, and decimal-support must be explicitly opted-in for parsing.

Parser Example:

// assuming Parser ctor is given a new overload or an optional parameter:
var parser = new Parser(parseOptions: new ParseOptions { DefaultNumericType = NumberType.Decimal });

// or assuming the Parse method is given a new overload or an optional parameter:
var expression = parser.Parse("2.087991 * 3.14159". new ParseOptions { DefaultNumericType = NumberType.Decimal });

// maybe add a static property as well:
Parser.DefaultParseOptions = new ParseOptions { DefaultNumericType = NumberType.Decimal };

// if ParseOptions is not specified, use 'new ParseOptions { DefaultNumericType = NumberType.Double }` by default.
var parser = new Parser();
var expression = parser.Parse("2.087991 * 3.14159");

For parameters, handling can be modified to transparently convert between double/decimal and preserve precision when mathing between the two value types by subclassing (i.e., DecimalNumberValue and DoubleNumberValue with NumberValue refactored to an abstract base) or internally within NumberValue using a variation on the Maybe monad (similar to OneOf, except limited to decimal/double). Because existing code is limited exclusively to double, there should be little risk of breaking changes to existing consumers on update.

Barring explicit support for decimal types, maybe an extension point that the various operation nodes can support to defer evaluation to another object?

Example:

// supporting custom value types for multiplication

// marker/functional interfaces defined within xFunc
public interface ICanMath { }
public interface ICanMultiply : ICanMath
{
    bool TryMultiplyAsLeftOperand(object right, out ICanMath result);
    bool TryMultiplyAsRightOperand(object left, out ICanMath result);
}

// example custom value type
public record DecimalValue(decimal Value) : ICanMultiply
{
    public bool TryMultiplyAsLeftOperand(object right, out ICanMath result) =>
        TryMultiply(this, right, out result);

    public bool TryMultiplyAsRightOperand(object left, [NotNullWhen(true)] out ICanMath result) =>
        TryMultiply(left, this, out result);

    private static bool TryMultiply(object left, object right, [NotNullWhen(true)] out ICanMath result)
    {
        result =
            TryGetDecimal(left, out decimal? leftOperand) && TryGetDecimal(right, out decimal? rightOperand)
                ? new DecimalValue(leftOperand.Value * rightOperand.Value)
                : null;

        return result != null;
    }

    private static bool TryGetDecimal(object value, [NotNullWhen(true)] out decimal? dec)
    {
        dec =
            value switch
            {
                decimal d => d,
                double dbl => (decimal)dbl,
                NumberValue nv => (decimal)nv.Number,
                DecimalValue dv => dv.Value,
                _ => null
            };

        return dec.HasValue;
    }

    public static implicit operator ParameterValue(DecimalValue decimalValue) => new ParameterValue(decimalValue);
    public static implicit operator DecimalValue(decimal value) => new DecimalValue(value);
}

// within xFunc.Maths.Expression.Mul.Execute:
return (leftResult, rightResult) switch
{
    (NumberValue left, NumberValue right) => left * right,
    ...
    (ICanMultiply left, object right) when left.TryMultiplyAsLeftOperand(right, out ICanMath result) => result,
    (object left, ICanMultiply right) when right.TryMultiplyAsLeftOperand(left, out ICanMath result) => result,
    _ => throw ExecutionException.For(this),
};

// within ParemeterValue
public ParameterValue(ICanMath value)
    : this(value as object)     // update internal ctor to recognize ICanMath as valid type
{
}

// parse/execute:
var p = new Parser();
var expression = p.Parse("2 * myVar");
var parameters = new ExpressionParameters { ["myVar"] = new DecimalValue(3.14159M) };
var result = expression.Execute(parameters);  // 'result' is an instance of DecimalValue

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureproposalFeatures that are not approved yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions