11

I am writing a library for our company's product that will take any kind of architectural dimension that our users are already familiar with as input for a function that converts from a string to a double. Here is the list of input types that we would like to be valid.

Input| Meaning | Output(Inches represented by a double)


12.5' | 12 Feet and six inches | 150.0

11" | 11 Inches | 11.0

3/16" | 3 sixteenths of an Inch | 0.1875


spaces may or may not be used between the feet and inches and the inches and sixteenths

11' 11" | 11 Feet and 11 Inches | 143.0

11'11" | 11 Feet and 11 Inches | 143.0


Dashes may or may not be used between feet and inches or between inches and sixteenths or both

12'-11" | 12 Feet and 11 Inches | 155.0

12' 11 3/16" | 12 Feet and 11 Inches and 3 sixteenths | 155.1875

12' 11-1/2" | 12 Feet and 11 Inches and 8 sixteenths | 155.5


Any number of spaces may be used between the feet and inches and the inches and sixteenths

12' 11 1/2" | 12 Feet and 11 Inches and 8 sixteenths | 155.5


An alternate simpler format is also available

121103 | 12 Feet and 11 Inches and 3 sixteenths | 155.1875


Negatives are also possible in every format

-121103 | 12 Feet and 11 Inches and 3 sixteenths | -155.1875

-11'11" | 11 Feet and 11 Inches | -143.0

We are currently using an extremely complicated set of branching logic to try and determine what format the input is trying to emulate... And it doesn't work in all cases.

Is there some possible combination of LINQ and Regular Expressions and witchcraft that we can use to determine how to parse the string?

Also note that we really want to avoid giving a simple combobox on the form to select the input format type from.

jth41
  • 3,808
  • 9
  • 59
  • 109
  • Do you work exclusively with Imperial measurements or do you also work with Metric? – Kyte Apr 01 '14 at 18:57
  • @Kyte, clearly he's an engineer. – tenub Apr 01 '14 at 19:31
  • This could be validated with regex. However, with so many optional formats, it would be hard for a user to remember input rules. –  Apr 01 '14 at 19:41
  • Thats just it, I dont want them to remember any rules other than what they already use daily (Autocad, other industry applications). I want my function to handle their inputs gracefully and I think it is possible because I have narrowed it to only the above possibilities – jth41 Apr 01 '14 at 19:42
  • 2
    I think you could do better with 3 input boxes. One for feet, inches, and and a combo box for fraction of inches. Use them together to determine a number. –  Apr 01 '14 at 19:48
  • @sln Please don't! I could live with multiple text boxes but a combo box? I can type a measurement in using 6 key presses. Using your model, I'd need: 2 keys, click, 2 keys, click, scroll, click. If you have to enter any number of measurements at the same time, it would be a usability nightmare. – Basic Nov 17 '15 at 02:09
  • @Basic - Well, I guess you could have 3 input boxes (no combo). I.e. `|______| ft. |______| in. |______| /16 in.` And just tab or shift-tab to the boxes. –  Nov 17 '15 at 02:18
  • @sln Better - Especially if typing 2 digits in one box moves focus to the next in an intelligent way (while handling backspace in reverse, etc). But yes, that would be an improvement over a drop-down. – Basic Nov 17 '15 at 02:20

2 Answers2

5

This function works for your input value examples.

public static Double Conv(String inp)
{
    String expr= "((?<feet>\\d+)(?<inch>\\d{2})(?<sixt>\\d{2}))|((?<feet>[\\d.]+)')?[\\s-]*((?<inch>\\d+)?[\\s-]*((?<numer>\\d+)/(?<denom>\\d+))?\")?";
    Match m = new Regex(expr).Match(inp);
    Double feet = m.Groups["feet"].Success ? Convert.ToDouble(m.Groups["feet"].Value) : 0;
    Int32  inch = m.Groups["inch"].Success ? Convert.ToInt32(m.Groups["inch"].Value) : 0;
    Int32  sixt = m.Groups["sixt"].Success ? Convert.ToInt32(m.Groups["sixt"].Value) : 0;
    Int32 numer = m.Groups["numer"].Success ? Convert.ToInt32(m.Groups["numer"].Value) : 0;
    Int32 denom = m.Groups["denom"].Success ? Convert.ToInt32(m.Groups["denom"].Value) : 1;
    return feet*12+inch+sixt/16.0+numer/Convert.ToDouble(denom);
}    

Please note that I haven't made any effort in testing other inputs than the valid ones you supplied. You may want to e.g. check for Success in at least some of the capture groups, or maybe do validation as a separate step. This code was made with parsing in mind.

Edit:

Here is a more robust version:

public static Double Conv(String inp)
{
    String expr= "^\\s*(?<minus>-)?\\s*(((?<feet>\\d+)(?<inch>\\d{2})(?<sixt>\\d{2}))|((?<feet>[\\d.]+)')?[\\s-]*((?<inch>\\d+)?[\\s-]*((?<numer>\\d+)/(?<denom>\\d+))?\")?)\\s*$";
    Match m = new Regex(expr).Match(inp);
    if(!m.Success || inp.Trim()=="")
    {
        // maybe throw exception or set/return some failure indicator
        return 0; // here using return value zero as failure indicator
    }
    Int32 sign  = m.Groups["minus"].Success ? -1 : 1;
    Double feet = m.Groups["feet"].Success ? Convert.ToDouble(m.Groups["feet"].Value) : 0;
    Int32  inch = m.Groups["inch"].Success ? Convert.ToInt32(m.Groups["inch"].Value) : 0;
    Int32  sixt = m.Groups["sixt"].Success ? Convert.ToInt32(m.Groups["sixt"].Value) : 0;
    Int32 numer = m.Groups["numer"].Success ? Convert.ToInt32(m.Groups["numer"].Value) : 0;
    Int32 denom = m.Groups["denom"].Success ? Convert.ToInt32(m.Groups["denom"].Value) : 1;
    return sign*(feet*12+inch+sixt/16.0+numer/Convert.ToDouble(denom));
}

It fails for empty strings, and strings with extra characters other than allowed by your examples. Five or more digits are treated as the simpler format.

The changes are the begin- and end anchors and allowed leading and trailing whitespace, as well as the special case check for emtpy/whitespace-only string in the if statement.

Disclaimer: this has obviously not been tested for every possible illegal input, and I am not a c# programmer anyway :-)

Trygve Flathen
  • 686
  • 7
  • 15
  • Everything seems to work great other than my handling of gibberish. How would you best suggest I handle that possibility? What could I do to make sure it is a valid string for this operation? – jth41 Apr 02 '14 at 18:39
  • Edited to include some validity checking. – Trygve Flathen Apr 02 '14 at 20:13
  • And added support for negative numbers. – Trygve Flathen Apr 02 '14 at 20:27
  • Thanks so much for your awesome answer! Everything works perfect except for when I get an input of just `"`. How can I add this simple check? It currently throws an error like I want it to with an input string of just `'` – jth41 Apr 02 '14 at 20:28
  • I guess the easiest would be to add more checks in the if statement: `if(!m.Success || inp.Trim()=="" || inp.Trim()=="\"")`. (the `Trim`ming should maybe be moved to the top of the function. That would also eliminate the need for leading and trailing whitespace in the regex. – Trygve Flathen Apr 02 '14 at 20:32
  • Awesome, I will do that – jth41 Apr 02 '14 at 20:39
  • Do you have any ideas of if it would be possible to make it so I could allow for any percision? so instead of only allowing "3/16" the user could type "3/32"? – jth41 Apr 02 '14 at 21:35
  • 1
    Instead of using `inp.Trim()==""`, use `String.IsNullOrWhiteSpace(inp)`. – Basic Nov 17 '15 at 02:12
1

This may move your complexity from the branching logic to the regex logic:

/(?<special>(?<feet>\d+)(?<inch>\d{2})(?<sixt>\d{2}))|((?<feet>[\d.]+)')?[\s-]*((?<inch>\d+)?[\s-]*((?<numer>\d+)\/(?<denom>\d+))?")?/

If group special matches, you have the special syntax, and output is feet*12+inch+_sixt_/16, using ToDecimal on the groups. If not, you will have one or more of the groups feet, inch, numer and denom if the input is valid. Use ToDouble for feet and ToDecimal for the rest, and make sure to check for division by zero in the fraction.

Proof-of-concept demonstration code (ruby):

[
  ["12.5' "," 12 Feet and six inches "," 150.0"],
  ["11\"  "," 11 Inches "," 11.0"],
  ["3/16\" "," 3 sixteenths of an Inch "," 0.1875"],
  ["11' 11\" "," 11 Feet and 11 Inches "," 143.0"],
  ["11'11\" "," 11 Feet and 11 Inches "," 143.0"],
  ["12'-11\" "," 12 Feet and 11 Inches "," 155.0"],
  ["12' 11 3/16\" "," 12 Feet and 11 Inches and 3 sixteenths "," 155.1875"],
  ["12' 11-1/2\" "," 12 Feet and 11 Inches and 8 sixteenths "," 155.5"],
  ["12'   11     1/2\" "," 12 Feet and 11 Inches and 8 sixteenths "," 155.5"],
  ["121103 "," 12 Feet and 11 Inches and 3 sixteenths "," 155.1875"],
  ["", "empty string", "0"],
].each{|i,d,o|
  m = /(?<special>(?<feet>\d+)(?<inch>\d{2})(?<sixt>\d{2}))|((?<feet>[\d.]+)')?[\s-]*((?<inch>\d+)?[\s-]*((?<numer>\d+)\/(?<denom>\d+))?")?/.match(i)
  #puts "#{(1..8).map{|n|"%15s"%m[n].inspect}.join}"
  puts "#{"%20s"%i}   #{"%10s"%o}   #{m[:special] ? m[:feet].to_i*12+m[:inch].to_i+m[:sixt].to_i/16.0 : m[:feet].to_f*12+m[:inch].to_i+(m[:numer].to_i.to_f/(m[:denom]||1).to_i)} "
}

output:

              12.5'         150.0   150.0 
               11"           11.0   11.0 
              3/16"        0.1875   0.1875 
            11' 11"         143.0   143.0 
             11'11"         143.0   143.0 
            12'-11"         155.0   155.0 
       12' 11 3/16"      155.1875   155.1875 
        12' 11-1/2"         155.5   155.5 
  12'   11     1/2"         155.5   155.5 
             121103      155.1875   155.1875 
                                0   0.0 

Please note that I haven't made any effort in testing other inputs than the valid ones you supplied. You may want to e.g. check for non-nil values in at least some of the capture groups, or maybe do validation as a separate step. This code was made with parsing in mind.

Trygve Flathen
  • 686
  • 7
  • 15