The little things in code are easily overlooked, and can be the source of problems which are difficult to diagnose. Consider the conversion of a string to a number:
1
|
d := StrToFloat(NumericString); |
What could go wrong? Well, many things, actually. In writing this, you no doubt were thinking of the problem at hand, rather than the general class of such problems. But truly, that is a mistake. You would always ensure that the content of the argument NumericString is of the correct form to be converted. But others may not. So although you would expect to pass in a value like ‘2345.27’, someone else may pass in ‘2,345.27’, provoking an exception. One approach to guarding against that could be:
1
2
3
4
|
try d := StrToFloat(NumericString); except end ; |
But that merely hides the exception. Would you not really prefer a better solution? The empty except clause is itself a code design error. There are many alternatives, among them:
1
2
3
4
5
6
7
|
var d: Double ; code: Integer ; begin Val (NumericString, d, code); if code <> 0 then Exit; |
Of course, this code ignores whatever is the purpose of the routine. However, it presents some advantages:
- Improper strings will not throw an exception.
- The value in code is zero if all is well, or is the index to the problem character in the string.
One thing to note is that Val() is a procedure which has been in Delphi forever. Now that we no longer get printed manuals, we really should spend some time in the libraries, or at least in the related help, to be fully aware of what we get from Delphi, without having to write our own code.
What other approaches could we consider in resolving this in a general way? We must first think of the possibilities, and of good design and coding practices. We could:
- Use a different conversion routine from the library, like TryStrToInt().
- Remove from the string all characters which are not in the set: [‘0’..’9′, ‘.’].
- Write a filter which handles the majority of reasonable possibilities.
Using TryStrToInt protects us from throwing an exception, and yields zero if the input was bad, but it’s not a real solution, in that it does nothing to help us understand the real problem.
The second and third possibilities are very similar, and worth considering:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
function NumOnly(ANumStr: string ): string ; var s: string ; cnt, idx, n: Integer ; begin n := ANumStr . Trim . Length; SetLength(s, n); cnt := 0 ; for idx := 1 to n do begin if CharInSet(ANumStr[idx], [ '0' .. '9' , '.' ]) then begin Inc(cnt); s[cnt] := ANumStr[idx]; end ; end ; SetLength(s, cnt); end ; |
This looks good, and we are then able to write something like:
1
|
d := StrToFloat(NumOnly(NumericString)); |
It will handle a wide range of numeric strings, and will return a good value. But it has weaknesses. What if the string looks like: ‘2235.30 – 27.57’? Well, it is not intended to be an expression evaluator, and it will then throw an exception, as it tries to convert ‘2235.3027.57’ to a number. So it is not a perfect solution, but it is a reasonable solution to a wide range of input.
In this kind of code, keep in mind that this should be in a library, so that it can serve your needs anywhere in the application. It should not be coded inline or in a nested function, since that approach will lead inevitably to copy and paste.
In pursuing the solution for your own needs, you will have to consider many issues, but the point is that doing such analysis is the difference between designing and merely coding.
There is a problem with code samples containing greater than or less than characters. They are not displayed correctly. E.g. in the second example , the if statement is invalid, because the comparison operator is not visible.
Thanks for the heads-up, Thomas. I have fixed that error, but clearly must check the code presentation more closely in future. The blog is new, and has presented some challenges. Trying to get things sorted out as quickly as possible.