Beware of the Past
Undiscovered things
A few days ago, Jolyon Smith warned Delphi users to Mind the Gap(s). The warning is about how assigning specific ordinal values to enum members defeats the RTTI system. He writes:This is what I love about Delphi. After almost 15 years of Delphi’ing there’s still new things to learn, and I don’t just mean new features in the latest releases. I mean, basic, fundamental things that have been there for years, just undiscovered (by me).I fully agree, and today it was my turn to find such an "undiscovered (by me)" thing in Delphi.
I received a bugreport about a simple edit screen not opening due to an EVariantInvalidArgError exception. Strange, because the exception seemed to be in straightforward code that has been in production for twelve odd years, and hasn't been changed lately (sound familiar?).
Examining the code
Leaving out the code that checks for invalid inputs, the remaining code basically boils down to this:var
EditText:
string
;StoredValue: Variant;
begin
// example user input from the edit screen
EditText :=
'16-9-2009'
;...
// store the date value when the edit screen closes
StoredValue := StrToDate(EditText);
...
// convert the stored value to string when the edit screen opens
EditText := VarToStr(StoredValue);
<= EVariantInvalidArgError reported ???
end
;Not much too worry about codewise it seems. And I couldn't reproduce the error using my development data. Still, the error was readily reproducible in the production environment, so I looked at the production database record to see what value was causing the trouble. It turned out to be '01-01-0010'. Clearly a typo by a user that wanted to write '01-01-2010'. A typo maybe, but a valid date nevertheless. And besides, if there really was anything fishy about that date, I would expect the exception to be raised when trying to store the value in the variant, not when converting the variant back to a string. What was happening?
Use the source, Luke
Well, if it isn't me, it must be Delphi. So I looked through the code in the variants unit and after a while it started to make sense. It turns out that Delphi uses an OS function (VarBstrFromDate) to cast the variant date value to a string. And this OS function only accepts dates from 01-01-100 onward, returning a E_INVALIDARG for earlier dates. So, although we can set a variant to a date before 01-01-100 and get the value from the variant as a date, we'll get an exception as soon as any implicit or explicit cast to string is performed (e.g. viewing the variant in the debugger or, the case at hand, using the VarToStr function).Feel Good Bugs
Being a responsible Delphi citizen, I submitted the bug to Quality Central (#77783). But as this bug has "extreme corner case" written all over it, I don't think it will be fixed anytime soon. So I rewrote my code to only accept dates from 01-01-100 onwards, and treat all dates before that as invalid. It was a simple change because the application had centralized input checking. Another bug bites the dust!I realize I just love this part of my work. Admittedly, there have been bugs in the past that I loved a lot less. But this was one of the "feel good" bugs. A bug that is brought to your attention by strange, apparently unexplainable symptoms. A bug that takes some effort (but no longer than say a few hours) to localize (and is preferably localized in some else's code), and finally a bug that is easy to repair or workaround.
Can't wait for the next one.
3 comments:
I have run into issues where the OS is referenced on several occasions, going back to DOS. Whilst it isn't always an option, I try to write my own code as far as possible. Whilst this may be "reinventing the wheel", on longer term projects it pays off.
It is one of the things I really dislike about Variants: it forces you to depend on conversion and or rounding oddities that you do not have influence on, nor can find the sourcecode for.
Therefore I still use typed data whenever possible.
--jeroen
My DUnit tests ran into exactly the same behaviour and I'm kind of glad that it happened. Since we also rely on database entries, the problem could have hit us exactly as you described it.
I also managed to trace the bug to the VarBStrFromDate function provided by the OS, but the crucial hint that this function only accepts values beyond 01/01/0100 was still missing. Your blog saved me a lot of work.... thanks (I was close to writing some test routines evaluating the dates i can throw at a variant without creating exceptions ;) )
Post a Comment