Search code examples
validationinputdartdecimalfractions

How can I convert and validate user inputted fractions and decimals, into a decimal, in Google's Dart programming language?


I would like an approach that takes a users inputted fraction or decimal, that then validates it (in a reasonably forgiving way), and then converts it to a decimal.

Similar questions have been asked for other programming languages, but they all appear to rely on eval() which doesn't appear to exist in Dart, allows only for fractions, or requires the input to already be valid.

I would like it to handle the users input in the following ways:

    //generic
    print("'5': expect:5, actual:" + fractionStringToNum("5").toString());
    print("'5/8': expect:0.625, actual:" + fractionStringToNum("5/8").toString());
    print("'5 5/8': expect:5.625, actual:" + fractionStringToNum("5 5/8").toString());
    print("'5 10/8': expect:6.25, actual:" + fractionStringToNum("5 10/8").toString());
    print("'5.625': expect:5.625, actual:" + fractionStringToNum("5.625").toString());
    print("'3 1/3': expect:3.33333, actual:" + fractionStringToNum("3 1/3").toString());
    print("'0.625': expect:0.625, actual:" + fractionStringToNum("0.625").toString());
    print("'.625': expect:0.625, actual:" + fractionStringToNum(".625").toString());
    print("'0': expect:0, actual:" + fractionStringToNum("0").toString());

    //negatives
    print("'-5': expect:-5, actual:" + fractionStringToNum("-5").toString());
    print("'- 5': expect:NaN, actual:" + fractionStringToNum("- 5").toString());
    print("'-5.625': expect:-5.625, actual:" + fractionStringToNum("-5.625").toString());
    print("'-5 5/8': expect:-5.625, actual:" + fractionStringToNum("-5 5/8").toString());
    print("'5/-8': expect:-0.625, actual:" + fractionStringToNum("5/-8").toString());
    print("'5 5/-8': expect:4.375, actual:" + fractionStringToNum("5 5/-8").toString());
    print("'-5/-8': expect:0.625, actual:" + fractionStringToNum("-5/-8").toString());
    print("'-5 5/-8': expect:-4.375, actual:" + fractionStringToNum("-5 5/-8").toString());
    print("'- 5 - 5/8': expect:NaN, actual:" + fractionStringToNum("- 5 - 5/8").toString());
    print("'--5': expect:NaN, actual:" + fractionStringToNum("--5").toString());
    print("'-5-': expect:NaN, actual:" + fractionStringToNum("-5-").toString());
    print("'-asdf': expect:NaN, actual:" + fractionStringToNum("-asdf").toString());
    print("'- asdf': expect:NaN, actual:" + fractionStringToNum("- asdf").toString());

    //spaces
    print("' 5 5/8': expect:5.625, actual:" + fractionStringToNum(" 5 5/8").toString());
    print("'5  5/8': expect:5.625, actual:" + fractionStringToNum("5  5/8").toString());
    print("'5 / 8': expect:0.625, actual:" + fractionStringToNum("5 / 8").toString());
    print("'5 5 / 8': expect:5.625, actual:" + fractionStringToNum("5 5 / 8").toString());
    print("'- 5 5/8': expect:NaN, actual:" + fractionStringToNum("- 5 5/8").toString());
    print("'5 5': expect:NaN, actual:" + fractionStringToNum("5 5").toString());
    print("'5 .8': expect:NaN, actual:" + fractionStringToNum("5 .8").toString());
    print("'5 5 5/8': expect:NaN, actual:" + fractionStringToNum("5 5 5/8").toString());
    print("'5 5.8': expect:NaN, actual:" + fractionStringToNum("5 5.8").toString());
    print("'5 5 .8': expect:NaN, actual:" + fractionStringToNum("5 5 .8").toString());

    //fractions
    print("'5/5.625': expect:0.88888, actual:" + fractionStringToNum("5/5.625").toString());
    print("'5.625/8': expect:0.703125, actual:" + fractionStringToNum("5.625/8").toString());
    print("'5/.625': expect:8, actual:" + fractionStringToNum("5/.625").toString());
    print("'.625/8': expect:0.078125, actual:" + fractionStringToNum(".625/8").toString());
    print("'0/8': expect:0, actual:" + fractionStringToNum("0/8").toString());
    print("'0.625/0.625': expect:1, actual:" + fractionStringToNum("0.625/0.625").toString());
    print("'.625/.625': expect:1, actual:" + fractionStringToNum(".625/.625").toString());
    print("'5/0': expect:Infinity, actual:" + fractionStringToNum("5/0").toString());
    print("'0/0': expect:NaN, actual:" + fractionStringToNum("0/0").toString());
    print("'1 0/8': expect:1, actual:" + fractionStringToNum("1 0/8").toString());
    print("'1 5/0': expect:Infinity, actual:" + fractionStringToNum("1 5/0").toString());
    print("'1 0/0': expect:NaN, actual:" + fractionStringToNum("1 0/0").toString());
    print("'1/1/1970': expect:NaN, actual:" + fractionStringToNum("1/1/1970").toString());
    print("'1/1 1': expect:NaN, actual:" + fractionStringToNum("1/1 1").toString());
    print("'5/8 5/8': expect:NaN, actual:" + fractionStringToNum("5/8 5/8").toString());

    //strings
    print("'': expect:NaN, actual:" + fractionStringToNum("").toString());
    print("'asdf': expect:NaN, actual:" + fractionStringToNum("asdf").toString());
    print("'5/asdf': expect:NaN, actual:" + fractionStringToNum("5/asdf").toString());
    print("'asdf/8': expect:NaN, actual:" + fractionStringToNum("asdf/8").toString());
    print("'asdf/ghjkl': expect:NaN, actual:" + fractionStringToNum("asdf/ghjkl").toString());
    print("'5 asdf/ghjkl': expect:NaN, actual:" + fractionStringToNum("5 asdf/ghjkl").toString());
    print("'asdf.625': expect:NaN, actual:" + fractionStringToNum("asdf.625").toString());
    print("'5.asdf': expect:NaN, actual:" + fractionStringToNum("5.asdf").toString());
    print("'1 / asdf': expect:NaN, actual:" + fractionStringToNum("1 / asdf").toString());

    //misc
    print("'.625.625': expect:NaN, actual:" + fractionStringToNum(".625.625").toString());
    print("'.625 .625': expect:NaN, actual:" + fractionStringToNum(".625 .625").toString());
    print("'5.625 5/8': expect:NaN, actual:" + fractionStringToNum("2.1 1/2").toString());
    print("'.625 5/8': expect:NaN, actual:" + fractionStringToNum("2.1 1/2").toString());

Solution

  • I guess the syntax you are trying to match is:

    fraction ::= signed_number
               | signed_number / signed_number
               | signed_number number / signed_number
    signed_number ::= number | '-' number
    number ::= digit* '.' digit+ | digit+
    

    with white-space allowed. When doing complex stuff, always ensure that you know what you are doing before you start. For parsing, that's usually about writing a grammar.

    Using RegExp for parsing (which isn't necessarily better, just shorter), I'd do:

    num fractionStringToNum(String string) {
      var numRE = r"\d*(?:\.\d+|\d)";
      var fracRE = "^\\s*(-)?($numRE)(?:\\s+($numRE))?(?:\\s*/\\s*(-)?($numRE))?\\s*\$";
      var re = new RegExp(fracRE);
      var match = re.firstMatch(string);
      if (match == null) return double.NAN;
      num result = num.parse(match[2]);
      if (match[3] != null) {
         double num2 = double.parse(match[3]);
         if (match[5] == null) return double.NAN;
         double num3 = double.parse(match[5]);
         // if (match[4] == '-') result = -result;  // Don't want to do this.
         result += num2 / num3;
      } else if (match[5] != null) {
         result /= double.parse(match[5]);
      }
      if (match[1] != match[4]) result = -result;
      return result;
    }
    

    This hits most of your test cases, but disagrees on "5 5/-8". I would just prefer to disallow that expression (a negative divisor when you have a compound expression), because I can't see any obvious interpretation of it - and more than one unobvious. I put the fix in the code, but commented it out because I don't like it :)