Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Regex Decimal Range Expression. Negative to Positive value

I am trying to make a regex expression that will validate a field (decimal) that should be in this range -12.0 to +13.0.

I was able to do this regex expression ^[-+]?(?:[0-9]|[0-1][0-2](?:[.][0-9])?)$ but it's not enough as this will allow -12.9 to +12.9 values and will not validate things like 0 or 0.0.

I know that this is not the best solution and a better approach will be to use the model and add the regex validation using Data Annotation.

Main problem here is that the Model.Value is a string. But I need to validate in many different ways that string before updating the database value. The way I validate that string is by using a foreign key that will tell me the regex expression to use.

Imagine this model.

ModelA

public virtual int Id{ get; set; }
public virtual IList<ModelB> ModelB{ get; set; }

Model B

public virtual string Value { get; set; }
public virtual DataType DataType{ get; set; }

And in the DataType I have the regex validation for that specific value

That is why I am using this approach...

Can someone help me accomplish this?

Thanks for all your help.

like image 951
legollas007 Avatar asked Mar 05 '26 02:03

legollas007


2 Answers

Because you're doing web client side validation you can't use Decimal.Parse() (or better Decimal.TryParse()) however Data Annotations already have what you need. Assuming you have this property in your ViewModel:

public class MyModel {
    [RegularExpression("^[-+]?(?:[0-9]|[0-1][0-2](?:[.][0-9])?)$")]
    public decimal Value { get; set; }
}

Yes, you may fix this regex however they're not the best tool to deal with numerical range validation, expression will quickly become terribly complex and - in general - it's not easy to evolve to changing requirements (what if you need to validate (-100..+1000.3]?) If regex is what you truly need (not just what you thought to use) then go with AdrianHHH's answer (and don't forget to include user's locale handling for decimal separator), if not (and assumption about your environment is valid) then use this:

public class MyModel {
    [Range(typeof(decimal), "-12", "13")]
    public decimal Value { get; set; }
}

That's all. Please note that if you had double instead of decimal then it may be further simplified to [Range(-12.0, 13.0)]. See MSDN for details.


EDIT: according to your edit your scenario is little bit more convoluted however I'd still suggest to do not misuse regex because of this, the proper approach (Data Annotations) is still a viable solution. First of all let's create a custom validation class:

[AttributeUsage(AttributeTargets.Property)]
public class ComplexLogicAttribute: ValidationAttribute, IClientValidatable
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var model = (MyModel)validationContext.ObjectInstance;

        if (IsValueValidForGivenObject(model, Convert.ToString(value)))
            return ValidationResult.Success;

        // Or your own error message...
        return new ValidationResult(FormatErrorMessage(null));
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var modelClientValidationRule = new ModelClientValidationRule
        {
            ValidationType = "complexlogic",
            ErrorMessage = ErrorMessage
        };

        return new List<ModelClientValidationRule> { modelClientValidationRule };
    }
}

Where IsValueValidForGivenObject() skeleton is:

private bool IsValueValidForGivenObject(MyModel model, string value)
{
    decimal minimum = CalculateMinimumAccordingToVeryComplexLogic(model);
    decimal maximum = CalculateMaximumAccordingToMagic(model);

    decimal number;
    if (!Decimal.TryParse(value, out number))
        return false;

    if (number < minimum || number > maximum)
        return false;

    return PerformEvenMoreComplexEsotericValidation(model, number);
}

Now you have to provide client side validation (let's say in ComplexLogic.js, don't forget to include it):

$.validator.addMethod("complexlogic", function (value, element, params) {
    // Repeat your validation logic here!
    return true;
});

$.validator.unobtrusive.adapters.add("complexlogic", function (options) {
    options.messages["complexlogic"] = options.message;
});

Take a look to this simple tutorial for a better explanation. That's all, you're validating numbers as numbers and your code is flexible enough...

like image 79
Adriano Repetti Avatar answered Mar 07 '26 15:03

Adriano Repetti


With a target like this I would break the problem in several smaller problems and then consider merging them to make a simpler regular expression.

First negative numbers: (-(12(\.0*)?|(1[01]|\d)(\.\d*)?|\.\d+)

-(                      # Always have a minus sign, then three alternatives.
     12(\.0*)?          # It can be `12` or `-12.` or `-12.000000`.
  |  (1[01]|\d)         # It can be a `10` or a `11` or any single digit,
             (\.\d*)?   #     optionally followed by a fractional part.
  |  \.\d+              # It can be just a fractional part.
 )

Zeroes and positive numbers are almost identical. The + is optional, the 13 is the special case and the two-digit set now includes 12 hence: \+?(13(\.0*)?|(1[012]|\d)(\.\d*)?|\.\d+)

We can then combine the two expressions as alternatives, giving:

((-(12(\.0*)?|(1[01]|\d)(\.\d*)?|\.\d+))|(\+?(13(\.0*)?|(1[012]|\d)(\.\d*)?|\.\d+)))

This has lots of captures that are not interesting. They could all be non-capturing but the expression is simpler without the extra ?: characters. But it is good to add them in at this stage. Leading to:

^((?:-(?:12(?:\.0*)?|(?:1[01]|\d)(?:\.\d*)?|\.\d+))|(?:\+?(?:13(?:\.0*)?|(?:1[012]|\d)(?:\.\d*)?|\.\d+)))$

The question is not clear on the treatment of leading zeros, i.e. whether values such as -00011.9 and 000.0001 and +012.3 are included or excluded. If leading zeroes are allowed then add a 0* after the - or +. Have also changed the \d for matching a single digit to [1-9] to avoid the ambiguity about where a single leading zero in e.g. 0.123 should be matched. The final expression is:

((?:-0*(?:12(?:\.0*)?|(?:1[01]|[1-9])(?:\.\d*)?|\.\d+))|(?:\+?0*(?:13(?:\.0*)?|(?:1[012]|[1-9])(?:\.\d*)?|\.\d+)))

Tested in C# with a selection of numbers, one per line, by using the regular expression

@"^((?:-0*(?:12(?:\.0*)?|(?:1[01]|[1-9])(?:\.\d*)?|\.\d+))|(?:\+?0*(?:13(?:\.0*)?|(?:1[012]|[1-9])(?:\.\d*)?|\.\d+)))$"

Responding to comments it seems that the requirement is a number that matches the regular expression [-+]\d\d?\.\d and that is in the range -12.0 <= number <= +13.0.

Reworking the last regular expression above to meet these needs gives the following. If a leading + is not allowed then omit the \+? from the expression.

((?:-(?:12\.0|(?:1[01]|\d)\.\d))|(?:\+?(?:13\.0|(?:1[012]|\d)\.\d)))

(Removed the leading zeroes; changed multiple fractional digits to be a single digit; removed the .\d option.)

like image 44
AdrianHHH Avatar answered Mar 07 '26 16:03

AdrianHHH