Paradox BCD field - Number is out of Range error - delphi

I'm attempting to update an old Paradox (BDE) table using an older Delphi XE6 application. One particular table has two fields; a Date field, and a BCD field. I'm unable to post a new record to the (FuelSurch) table due to the BCD (FuelSur) field which is throwing a "Number is out of range" error.
So I wrote a quick test, but still the same issue. Here are the FuelSurch table field specifications.
I just added a button to a form and this procedure fires when clicked.
procedure TTestForm.BCDTestBtnClick(Sender: TObject);
var
Bcd: TBcd;
begin
POE_Data.FuelSurch.Open;
POE_Data.FuelSurch.Insert;
Bcd:= StrToBcd('2.01');
showMessage('Bcd = ' + BcdToStr(Bcd)); // This works and shows 'Bcd = 2.01'
POE_Data.FuelSurchFuelSur.AsBCD := Bcd; // Line produces "Number is out of range." error
POE_Data.FuelSurch.Post;
POE_Data.FuelSurch.Close;
end;
The database connection is confirmed as good as I'm able to post to other tables. It just seems like this BCD field type is giving me issues.
Do I need to format the string differently before assigning it to the Bcd variable?

Internally the precision of a BCD is the total number of digits, not the number of digits after the decimal separator.
It's always an even number (when odd, 1 is added to it). The reason is that a byte stores 2 digits.
So if you try this:
procedure TForm1.Button1Click(Sender: TObject);
var
Bcd: TBCD;
begin
Bcd := StrToBcd('2.01', TFormatSettings.Invariant);
ShowMessage(IntToStr(Bcd.Precision));
end;
you will see 4, because 3 digits are used, plus 1 to make it even.
Your field precision is only 2. If it represents the same as TBCD.Precision, it's not enough.
Note that BcdPrecision() returns the number of digits before the decimal separator, which is a bit confusing.

Related

Delphi - SysUtils.Trim not deleting last space(?) char

Delphi RIO. I have built an Excel PlugIn with Delphi (also using AddIn Express). I iterate through a column to read cell values. After I read the cell value, I do a TRIM function. The TRIM is not deleting the last space. Code Snippet...
acctName := Trim(UpperCase(Acctname));
Before the code, AcctName is 'ABC Holdings '. It is the same AFTER the TRIM function. It appears that Excel has added some type of other char there. (new line?? Carriage return??) What is the best way to get rid of this? Is there a way I can ask the debugger to show me the HEX value for this variable. I have tried the INSPECT and EVALUATE windows. They both just show text. Note that I have to be careful of just deleting NonText characters, and some companies names have dashes, commas, apostrophes, etc.
**Additional Info - Based on Andreas suggestion, I added the following...
ShowMessage(IntToHex(Ord(Acctname[Acctname.Length])));
This comes back with '00A0'. So I am thinking I can just do a simple StringReplace... so I add this BEFORE Andreas code...
acctName := StringReplace(acctName, #13, '', [rfReplaceAll]);
acctName := StringReplace(acctName, #10, '', [rfReplaceAll]);
Yet, it appears that nothing has changed. The ShowMessage still shows '00A0' as the last character. Why isn't the StringReplace removing this?
If you want to know the true identity of the last character of your string, you can display its Unicode codepoint:
ShowMessage(IntToHex(Ord(Acctname[Acctname.Length]))).
Or, you can use a utility to investigate the Unicode character on the clipboard, like my own.
Yes, the character in question is U+00A0: NO-BREAK SPACE.
This is like a usual space, but it tells the rendering application not to put a line break at this space. For instance, in Swedish, at least, you want non-breaking spaces in 5 000 kWh.
By default, Trim and TStringHelper.Trim do not remove this kind of whitespace. (They also leave U+2007: FIGURE SPACE and a few other kinds of whitespace.)
The string helper method has an overload which lets you specify the characters to trim. You can use this to include U+00A0:
S.Trim([#$20, #$A0, #$9, #$D, #$A]) // space, nbsp, tab, CR, LF
// (many whitespace characters missing!)
But perhaps an even better solution is to rely on the Unicode characterisation and do
function RealTrimRight(const S: string): string;
var
i: Integer;
begin
i := S.Length;
while (i > 0) and S[i].IsWhiteSpace do
Dec(i);
Result := Copy(S, 1, i);
end;
Of course, you can implement similar RealTrimLeft and RealTrim functions.
And of course there are many ways to see the actual string bytes in the debugger. In addition to writing things like Ord(S[S.Length]) in the Evaluate/Modify window (Ctrl+F7), my personal favourite method is to use the Memory window (Ctrl+Alt+E). When this has focus, you can press Ctrl+G and type S[1] to see the actual bytes:
Here you see the string test string. Since strings are Unicode (UTF-16) since Delphi 2009, each character occupies two bytes. For simple ASCII characters, this means that every second byte is null. The ASCII values for our string are 74 65 73 74 20 73 74 72 69 6E 67. You can also see, on the line above (02A0855C) that our string object has reference count 1 and length B (=11).
As a demo, to show the unicode string:
program q63847533;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils;
type
array100 = array[0..99] of Byte;
parray100 = ^array100;
var
searchResult : TSearchRec;
Name : string;
display : parray100 absolute Name;
dummy : string;
begin
if findfirst('z*.mp3', faAnyFile, searchResult) = 0 then
begin
repeat
writeln('File name = '+searchResult.Name);
name := searchResult.Name;
writeln('File size = '+IntToStr(searchResult.Size));
until FindNext(searchResult) <> 0;
// Must free up resources used by these successful finds
FindClose(searchResult);
end;
readln(dummy);
end.
My directory contains two z*.mp3 files, one with an ANSI name and the other with a Unicode name.
WATCHing display^ as Hex or Memorydump will display what you seem to require (the Is there a way I can ask the debugger to show me the HEX value for this variable. of your question)

TClientDataset: Is there a way to make a StringField behave like an IntegerField when setting an index?

I want to sort a ClientDataset by a StringField which contains Integers. These Integers are used as item numbers on invoices. When I sort the ClientDataset by that field using a simple index the result is this:
1
10
100
101
11
110
111
12
120
However I'd like them to be sorted like an IntegerField:
1
10
11
12
100
101
110
111
120
Is there a quick way to do this?
You don't need to change your existing field to a different type. Instead, a way to do what you want is to define an ftInteger field on the CDS which is of FieldFind fkInternalCalc. Then in the CDS's OnCalcFields event, set the field's value by converting the value of your string field to an integer.
The point of setting the calculated field's find tp fkInternalCalc is that the CDS can be indexed on, and hence sorted by, an fkInternalCalc field, unlike an fkCalculated field. So, by indexing your CDS on the added fkInternalCalc field, the records should be appear in their numeric order rather that the alphanumeric order of your string field.
Code:
procedure TForm1.CDS1CalcFields(DataSet: TDataSet);
begin
if CDS1IntField.IsNull then
CDS1IntField.AsInteger := StrToInt(CDS1StringField.AsString);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
i : Integer;
begin
CDS1.CreateDataSet;
for i := 1 to 100 do
CDS1.InsertRecord([IntToStr(i)]);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
CDS1.IndexFieldNames := 'IntField';
end;
If you want to sort as integers: by far the best approach is to: change the type to integer.
Yes, it's more work, but hear me out.
Currently you have 2 possibilities: either all your values are currently canonical1 integers, or they are not.
If they are, then you do have a little work to do. You need to change:
The column in the database
Any foreign key columns containing the same data
The types of any persisted fields for those columns
Any code that is expecting string item numbers
And you'll gain the following benefits:
Better performance due to: single op comparisons, less data in memory, narrower keys for B-Tree indexes and reduced heap memory allocations in Delphi for the previous long string objects.
Less concern about the nuances of strings and string comparison. E.g. unexpected padding.
And a strong typing guarantee that you'll not unexpectedly have to deal with 'problematic strings'. You raised a concern whether you would "have any negative consequences" due to making the change. I'm not aware of any. However, not making the change does risk the consequence of unexpected non-integer item numbers.
However, if your item numbers are not canonical integers: then trying to sort as integers is already problematic. How would you sort the following item numbers as integers?
'03', '13', '3', 'B2', ' 13 ', '3bad'
Martyn's answer would be the best way to go in this situation. Because you retain the original string value, you still have a safe and reliable key. (NB only use the internal calc field for sorting).
Note, you would have to tweak the: CDS1IntField.AsInteger := StrToInt(CDS1StringField.AsString); line to ensure you can set sensible sorting values. Any exceptions raised due to failure to convert some string would lead to nasty problems.
As a bonus, Martyn's answer is safe in the short term before you've investigated the knock-on effects of the larger change. But you may want to schedule time for the 'big job'.
1 The point about 'canonical' integers is subtle but important. Prefixing any number of zero's or spaces to an integer produces a non-canonical duplicate. Consider "3" and "03". As integers they are the same, but as strings they are different.

Converting Integer number into hexadecimal number in delphi 7

Write a program to convert an integer number to its hexadecimal representation without using inbuilt functions.
Here is my code, but it is not working. Can anyone tell where is the mistake?
It is giving an error:
"Project raised exception class EAccessViolation with message 'Access violation at address 00453B7B in module 'Project.exe'.Write of address FFFFFFFF'.Process stopped.Use Step or Run to continue."
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,Forms,
Dialogs;
type
TForm1 = class(TForm)
end;
function hexvalue(num:Integer):Char;
var
Form1: TForm1;
implementation
{$R *.dfm}
function hexvalue(num:Integer):Char;
begin
case num of
10: Result:='A';
11: Result:='B';
12: Result:='C';
13: Result:='D';
14: Result:='E';
15: Result:='F';
else Result:=Chr(num);
end;
end;
var
intnumber,hexnumber,actualhex:String;
integernum:Integer;
i,j,k:Byte;
begin
InputQuery ('Integer Number','Enter the integer number', intnumber);
integernum:=StrToInt(intnumber);
i:=0;
while integernum >= 16 do
begin
hexnumber[i]:=hexvalue(integernum mod 16);
integernum:= integernum div 16;
Inc(i);
end;
hexnumber[i]:= hexvalue(integernum);
k:=i;
for j:=0 to k do
begin
actualhex[j]:= hexnumber[i];
Dec(i);
end;
ShowMessage(actualhex);
end.
Since this obviously is a homework assignment, I don't want to spoil it for you and write the solution, but rather attempt to guide you to the solution.
User input
In real code you would need to be prepared for any mistake from the user and check that the input really is integer numbers only and politely ask the user to correct the input if erroneous.
Conversion loop
You have got that OK, using mod 16 for each nibble of integernum and div 16 to move to the next nibble, going from units towards higher order values.
Conversion of nibble to hex character
Here you go wrong. If you would have written out also the cases for 0..9, you could have got the case statement right. As others have commented, Chr() takes an ASCII code. However, using a case statement for such a simple conversion is tedious to write and not very efficient.
What if you would have a lookup table (array) where the index (0..15) directly would give you the corresponding hex character. That would be much simpler. Something like
const
HexChars: array[_.._] of Char = ('0',_____'F')
I leave it to you to fill in the missing parts.
Forming the result (hex string)
Your second major mistake and the reason for the AV is that you did not set the length of the string hexnumber before attempting to acess the character positions. Another design flaw is that you fill in hexnumber backwards. As a result you then need an extra loop where you reverse the order to the correct one.
There are at least two solutions to solve both problems:
Since you take 32 bit integer type input, the hex representation is not more than 8 characters. Thus you can preset the length of the string to 8 and fill it in from the lower order position using 8 - i as index. As a final step you can trim the string if you like.
Don't preset the length and just concatenate as you go in the loop hexnumber := HexChars[integernum mod 16] + hexnumber;.
Negative values
You did not in any way consider the possibility of negative values in your code, so I assume it wasn't part of the task.
First mistake : String are 1 indexed. Meaning that the index of their first character is 1 and not 0. You initialize "i" to 0 and then try to set hexnumber[i].
Second mistake : Strings might be dynamic, but they don't grow automatically. If you try to access the first character of an empty string, it won't work. You need to call SetLength(HeXNumber, NumberOfDigits). You can calculate the number of digits this way :
NumberOfDigits := Trunc(Log16(integernum)) + 1;
Since Log16 isn't really something that exists, you can either use LogN(16,integernum) or (Log(IntegerNum) / Log(16)) depending on what is available in your version of Delphi.
Note that this might return an invalid value for very, very large value (high INT64 range) due to rounding errors.
If you don't want to go that road, you could replace the instruction by
hexnumber := hexvalue(integernum mod 16) + hexnumber;
which would also remove the need to invert the string at the end.
Third Mistake : Using unsigned integer for loop variable. While this is debatable, the instruction
for I := 0 to Count - 1 do
is common practice in Delphi without checking Count > 0. When count = 0 and using an unsigned loop counter, you'll either get an integer overflow (if you have them activated in your project options) or you'll loop High(I) times, which isn't what you want to be doing.
Fourth mistake : already mentionned : Result:=Chr(num) should be replaced by something like Result := InttoStr(Num)[1].
Personally, I'd implement the function using an array.
HexArr : Array[0..15] of char = ('0', '1',...,'D','E','F');
begin
if InRange(Num, 0, 15) then
Result := HexArr[Num]
else
//whatever you want
end;

Locate in Delphi XE2 TADO exception with single quote in search value

Having trouble with Delphi XE2 (update2) using MS SQL 2008 R2 (sp 2) or with MS SQL 2005 (sp4).
form1.ado_m is via the TADO dataset with no parameter check using native 64 sql
var
okd:boolean;
dd:ansistring;
code snippet:
okd:=form1.ado_m.Locate('abcrow',dd,[loCaseInsensitive]);
If the value of dd ends in a single quote (it is converted to '' in the debugger) it gives a exception:
Exception class EOleException with message 'Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another'.
What is strange to me is that is a single quote character is the middle of var dd it works fine.
Workaround?
The problem is caused by the number sign (#) AND quote together in the search value.
Locate use the ADO find method: The value in Criteria may be a string, floating-point number, or date. String values are delimited with single quotes or "#" (number sign) marks (for example, "state = 'WA'" or "state = #WA#").
Try to replace the number sign OR quote character by inserting records.
Another option is to use the OnFilterRecord and filtered property:
...
FSearchValue := 'A1020778014#;]_69BO'''; // private field in TForm1
ado_m.Filtered := true;
try
okd := not ado_m.IsEmpty;
finally
ado_m.Filtered := false;
end;
...
procedure TForm1.ado_mFilterRecord(DataSet: TDataSet;
var Accept: Boolean);
begin
Accept := SameText(DataSet.FieldByName('abcrow').AsString, FSearchValue);
end;

Getting a Field List from a DBExpress TSQLQuery

I am having a problem getting a list of fields from a query defined at run time by the users of my program. I let my users enter a SQL query into a memo control and then I want to let them go through the fields that will return and do such things as format the output, sum column values and so forth. So, I have to get the column names so they have a place to enter the additional information.
I would do fine if there were no parameters, but I also have to let them define filter parameters for the query. So, if I want to set the parameters to null, I have to know what the parameter's datatype is.
I am using Delphi 2006. I connect to a Firebird 2.1 database using the DBExpress component TSQLConnection and TSQLQuery. Previously, I was successful using:
for i := 0 to Qry.Params.Count - 1 do Qry.Params[i].value := varNull;
I discovered I had a problem when I tried to use a date parameter. It was just a coincidence that all my parameters up until then had been integers (record IDs). It turns out that varNull is just an enumerated constant with a value of 1 so I was getting acceptable results (no records) was working okay.
I only need a list of the fields. Maybe I should just parse the SELECT clause of the SQL statement. I thought setting Qry.Prepared to True would get me a list of the fields but no such luck. It wants values for the parameters.
If you have an idea, I would sure like to hear it. Thanks for any help.
Replied again 'coz I'm interested. My methods works (with my queries) because they have been pre-defined with the params' datatypes preset to the correct type:)
I'm not sure how you are expecting the query to know or derive the datatype of the param given that you are not even selecting the field that it operates against.
So I think your query setup and user input method will need more attention. I've just looked up how I did this a while ago. I do not use a parameterised query - I just get the "parameter values" from the user and put them directly into the SQL. So your sql would then read:
SELECT s.hEmployee, e.sLastName
FROM PR_Paystub s
INNER JOIN PR_Employee e ON e.hKey = s.hEmployee
WHERE s.dtPaydate > '01/01/2008'
therefore no parameter type knowledge is necessary. Does not stop your users entering garbage but that goes back to input control :)
Although a slightly different dataset type this is what I use with TClientDataset simple and effective :)
for i := 0 to FilterDataSet.Params.Count -1 do
begin
Case FilterDataSet.Params.Items[i].Datatype of
ftString:
ftSmallint, ftInteger, ftWord:
ftFloat, ftCurrency, ftBCD:
ftDate:
ftTime:
ftDateTime:
.
.
.
end;
end;
can you not do something similar with the query?
You guys are making this way too hard:
for i := 0 to Qry.Params.Count - 1 do begin
Qry.Params[i].Clear;
Qry.Params[i].Bound := True;
end;
I'm not sure what version of Delphi you are using. In the Delphi 2006 help under Variant Types, it says:
Special conversion rules apply to the
Borland.Delphi.System.TDateTime type
declared in the System unit. When a
Borland.Delphi.System.TDateTime is
converted to any other type, it
treated as a normal Double. When an
integer, real, or Boolean is converted
to a Borland.Delphi.System.TDateTime,
it is first converted to a Double,
then read as a date-time value. When a
string is converted to a
Borland.Delphi.System.TDateTime, it is
interpreted as a date-time value using
the regional settings. When an
Unassigned value is converted to
Borland.Delphi.System.TDateTime, it is
treated like the real or integer value
0. Converting a Null value to Borland.Delphi.System.TDateTime raises
an exception.
The last sentence seems important to me. I would read that as varNull cannot be converted to a TDateTime to put into the field, and hence you get the exception that you're experiencing.
It also implies that this is the only special case.
Couldn't you do something like:
for i := 0 to Qry.Params.Count - 1 do
begin
if VarType(Qry.Params[i].value) and varTypeMask = varDate then
begin
Qry.Params[i].value := Now; //or whatever you choose as your default
end
else
begin
Qry.Params[i].value := varNull;
end;
end;
What I ended up doing was this:
sNull := 'NULL';
Qry.SQL.Add(sSQL);
for i := 0 to Qry.Params.Count - 1 do begin
sParamName := Qry.Params[i].Name;
sSQL := SearchAndReplace (sSQL, ':' + sParamName, sNull, DELIMITERS);
end;
I had to write SearchAndReplace but that was easy. Delimiters are just the characters that signal the end of a word.
TmpQuery.ParamByName('MyDateTimeParam').DataType := ftDate;
TmpQuery.ParamByName('MyDateTimeParam').Clear;
TmpQuery.ParamByName('MyDateTimeParam').Bound := True;

Resources