I am writing a data import utility to import data into a database application. The data can be retrieved from the clipboard (e.g. from Excel) or a file and is initially stored in a TStringlist. In the next step the user can select the appropriate column separator (tab, comma, etc.). After splitting the data into columns (using the selected separator), each string value (let's call it a cell value) is checked for validity against the applicable database field type.
The issue I'm having is with datetime values (and possibly date and/or time value, but I haven't checked that yet). If the user selects the "wrong" separator, the data is not split and each line contains a single column (which is correct, of course). In such a case, the cell value may contain a string like the one below (I am showing the debug value to show the correct separator, tab in this case):
'04/01/10 00:00'#9'2.50'#9'100'#9'Text value'
When using TryStrToDateTime or StrToDatetime, this string value "passes" because the string is "clipped" (i.e. ignoring the trailing text and returning the correct datetime value of 04/01/10). If I then, at a later stage, pass the cell value (the original string) as a variant to a range comparison function, it obviously fails with an EVariantTypeCastError.
Is there a method (or existing Delphi RTL function) to check for the string value to contain a valid date(time) only (i.e. no trailing text)? Unable to find a function (or function parameter) taking this into account, I have also been thinking about checking string length but my software is used internationally and the datetime format will therefore be varying and may thus have different lengths.
PS: I have added below sample code here as I couldn't add it to my comments.
procedure TForm1.Button1Click(Sender: TObject);
var
lStrVal: string;
lIsDateTime: boolean;
lDateTimeVal: TDateTime;
begin
lStrVal := '01/01/2019 10:00' + chr(9) + '120.00' + chr(9) + 'Some text';
lIsDateTime := TryStrToDateTime(lStrVal, lDateTimeVal);
if lIsDateTime then
messageDlg('String value is a date/time! ' + #13#10 + 'String: ' + lStrVal + #13#10 + 'Date/time is: ' + DateTimeToStr(lDateTimeVal), mtInformation, [mbOK], 0)
else
messageDlg('String value cannot be converted to a date/time!', mtWarning, [mbOK], 0);
end;
It appears that the solution (at least for my application) is to combine TryStrToDateTime and TryStrToDate. To demonstrate, start a new VCL application and drop a ListBox and TButton on the form (leave standard names). Paste the following code in the TButton OnClick handler:
var
lDateVal, lDateTimeVal: TDateTime;
lStrVal: string;
lResult: boolean;
begin
ListBox1.Items.Clear;
lStrVal := '01/01/2019 10:00' + '-120.00' + '-Some text';
ListBox1.Items.Add('String value : ' + lStrVal);
lDateVal := EncodeDate(2000, 1, 1);
lDateTimeVal := EncodeDate(2000, 1, 1) + EncodeTime(1, 0, 0, 0);
lResult := TryStrToDate(lStrVal, lDateVal);
if lResult then
ListBox1.Items.Add(' - TryStrToDate : TRUE')
else
ListBox1.Items.Add(' - TryStrToDate : FALSE');
ListBox1.Items.Add('DateTime value: ' + DateTimeToStr(lDateVal));
lResult := TryStrToDateTime(lStrVal, lDateTimeVal);
if lResult then
ListBox1.Items.Add(' - TryStrToDateTime : TRUE')
else
ListBox1.Items.Add(' - TryStrToDateTime : FALSE');
ListBox1.Items.Add('DateTime value: ' + DateTimeToStr(lDateTimeVal));
// Reset
lDateVal := EncodeDate(2000, 1, 1);
lDateTimeVal := EncodeDate(2000, 1, 1) + EncodeTime(1, 0, 0, 0);
lResult := TryStrToDateTime(lStrVal, lDateTimeVal) and TryStrToDate(lStrVal, lDateVal);
if lResult then
ListBox1.Items.Add(' - Combined : TRUE')
else
ListBox1.Items.Add(' - Combined : FALSE');
ListBox1.Items.Add('DateTime value: ' + DateTimeToStr(lDateTimeVal));
ListBox1.Items.Add('');
lStrVal := '01/01/2019 10:00';
ListBox1.Items.Add('String value : ' + lStrVal);
lResult := TryStrToDateTime(lStrVal, lDateTimeVal) and TryStrToDate(lStrVal, lDateVal);
if lResult then
ListBox1.Items.Add(' - Combined : TRUE')
else
ListBox1.Items.Add(' - Combined : FALSE');
try
lDateTimeVal := VarToDateTime(lStrVal);
lResult := true;
except
lResult := false;
end;
if lResult then
ListBox1.Items.Add(' - Using Variant : TRUE')
else
ListBox1.Items.Add(' - Using Variant : FALSE');
This leaves me with the question if this means TryStrToDateTime is incorrectly implemented in comparison to TryStrToDate? As a minimum there seems to be a contradiction on the "design" of the functions; "string value is a Date" versus "string value begins with a DateTime". Unless I am missing something...
Related
Delphi Tokyo - I am using Delphi to do some Excel spreadsheet pre-processing prior to a load operation. I am trying to read an Excel Range into a VarArray, loop through the array to do cleanup (in this case, it is a zip code column. If it is a 4 digit zipcode, prefix a '0'), and then write the VarArray back to the Excel range. Everything compiles, but I get an error message on the first access of the VarArray. The specific error is 'Variant or safe array index out of bounds'. My VarArray starts at row 2 of the excel range. Any idea why I am getting this error?
I have tried to simplify the code as much as possible here....
function PROCESS_ZIP_CODE_5DIGIT_MIN(P1,P2 : String): integer;
var
MyColumnLetter : String;
thisSheet : _Worksheet;
i : Integer;
CellText : String;
arrData: Variant;
myRange : ExcelRange;
RangeStartAddr, RangeEndAddr : String;
begin
MyColumnLetter := 'H';
thisSheet := oExcel.ActiveSheet as _Worksheet;
{create variant array where we'll copy our data}
arrData := VarArrayCreate([2, 500 ], varVariant);
// Get the Range Address
RangeStartAddr := MyColumnLetter + '2';
RangeEndAddr := MyColumnLetter + IntToStr(500);
// Now read the data into the VarArray
myRange := thisSheet.range[RangeStartAddr, RangeEndAddr];
arrData := myRange.Value2;
// Now process the data itself
for i := 2 to 500 do
begin
CellText := arrData[i]; // ERROR ON THIS LINE
if Length(CellText) = 4 then
begin
CellText:= '0' + CellText;
arrData[i] := CellText;
end;
end;
// Now write the VarArray back to the spreadsheet
thisSheet.range[RangeStartAddr, RangeEndAddr].Value2 := myRange;
end;
I'm not going to try to sort out your code, because it has a bunch of errors in it.
Here's a working sample of code to retrieve a range of cells (in this case, H1 through the last populated cell in J) into a variant array and then put that array into a Delphi TStringGrid. While the code uses late binding instead of early binding, it pretty clearly demonstrates the proper use of VarArrayCreate when reading a range from Excel.
var
Excel, Book, Sheet, Range1: OleVariant;
i, j: Integer;
Data: Variant;
const
// Obtained at https://msdn.microsoft.com/en-us/library/office/ff820880.aspx
xlDown = -4121;
begin
Excel := CreateOleObject('Excel.Application');
try
Book := Excel.WorkBooks.Open('E:\TempFiles\Test.xlsx');
Sheet := Book.Worksheets.Item['Sheet1'];
// Get tne range we want to extract, in this case all rows of columns H-J.
// .End(xlDown) finds the last used cell in the indicated column
Range1 := Sheet.Range['H1', Sheet.Range['J1'].End[xlDown]];
Data := Range1.Value;
// Get the number of columns and rows from the array itself. The addition
// of 1 is for the fixed row and column, and to synch up with the Data
// array being 1 based instead of 0
StringGrid1.ColCount := VarArrayHighBound(Data, 2) + 1;
StringGrid1.RowCount := VarArrayHighBound(Data, 1) + 1;
// StringGrid.Cells are accessed in Col, Row order, but the
// array is returned in Row, Col layout. Note the swap in
// i and j below in the subscripts to accomodate that fact.
for i := 1 to StringGrid1.ColCount - 1 do
for j := 1 to StringGrid1.RowCount - 1 do
StringGrid1.Cells[i, j] := Data[j, i];
finally
// Clean up all references so Excel will close cleanly
Range1 := null;
Sheet := null;
Book := null;
Excel.Quit;
Excel := null;
end;
This is something that should be easey but I just can´t get it work.
I come from java so maby I have a error in my thinking here.
What I want to do is that I have a string with two letters like 't4' or 'pq'.
Now I just want to get each of the chracters in the string as an own string.
So I do:
firstString := myString[0];
but I don´t even get this compiled.
So I figured that they start counting form 1 and put 1 as an index.
Now I do this in a while loop and the first time I go through it it works fine. Then the second time the results are just empty or wrong numbers.
What am I missing here?
(I also tried copy but that doesn´t work either!)
while i < 10 do
begin
te := 'te';
a := te[1];
b := te[2];
i := i +1;
end;
the first loop a is 't' and b is 'e' as I would expect. The second time a is '' and b ist 't' which I don´t understand!
Strings are 1-based, not zero-based. Try the following, after adding StrUtils to your Uses list (for DupeString):
var
MyString : String;
begin
MyString := '12345';
Caption := StringOfChar(MyString[1], 8) + ':' + DupeString(Copy(MyString, 3, 2), 4);
You could split it up to mke it easier to follow, of course:
var
MyString,
S1,
S2,
S3: String;
begin
MyString := '12345';
S1 := StringOfChar(MyString[1], 8);
S2 := Copy(MyString, 3, 2);
S3 := DupeString(S2, 4);
Caption := S1 + ':' + S3;
I am trying to get a particular integer after a sub string in InnoSetup in a line. There are Trim, TrimLeft, TrimRight functions but no substring extraction functions.
Example :
line string: 2345
line another string: 3456
I want to extract "2345" and "3456".
I am loading the file content in a array but can't get it de-referenced by array[count][char_count].
I would load the input file to a TStrings collection and iterate it line by line. For each line I would find a ":" char position and from this position I would copy the string which I would finally trim. In steps:
1. line string: 2345
2. 2345
3. 2345
Now remains to convert such string to an integer and add it to the final collection. Since you've shown an empty line in your file sample, and since you didn't said if this format will always be fixed, let's convert this string in the safe way. Inno Setup provides for this safe conversion just one function, the StrToIntDef function. This function however requires a default value, which is returned when the conversion fails and so you will have to pass to its call a value, which you'll never expect in your file. In the following example I chose -1, but you can replace it with any other value that you never expect in your input file:
[Code]
type
TIntegerArray = array of Integer;
procedure ExtractIntegers(Strings: TStrings; out Integers: TIntegerArray);
var
S: string;
I: Integer;
Value: Integer;
begin
for I := 0 to Strings.Count - 1 do
begin
// trim the string copied from a substring after the ":" char
S := Trim(Copy(Strings[I], Pos(':', Strings[I]) + 1, MaxInt));
// try to convert the value from the previous step to integer;
// if such conversion fails, because the string is not a valid
// integer, it returns -1 which is treated as unexpected value
// in the input file
Value := StrToIntDef(S, -1);
// so, if a converted value is different from unexpected value,
// add the value to the output array
if Value <> -1 then
begin
SetArrayLength(Integers, GetArrayLength(Integers) + 1);
Integers[GetArrayLength(Integers) - 1] := Value;
end;
end;
end;
procedure InitializeWizard;
var
I: Integer;
Strings: TStringList;
Integers: TIntegerArray;
begin
Strings := TStringList.Create;
try
Strings.LoadFromFile('C:\File.txt');
ExtractIntegers(Strings, Integers);
for I := 0 to GetArrayLength(Integers) - 1 do
MsgBox(IntToStr(Integers[I]), mbInformation, MB_OK);
finally
Strings.Free;
end;
end;
I have the text inside TEdit box:
'955-986, total = 32'
How would I delete all text after the comma, so it will only left '955-986'
I tried to limit the TEdit Length, but it's not working as I wanted it to be.
What if there'd be no comma? full non-cut string or empty string ?
Below is your idea of limiting string length, but only applied if at least one comma was found.
var
tmpStr:string;
commaPosition:integer;
begin
tmpStr := Edit1.Text;
commaPosition := pos(',',tmpStr);
if commaPosition > 0 then begin
SetLength(tmpStr, commaPosition - 1);
Edit1.Text := tmpStr;
end;
end;
You could use this code:
var
tmpStr:string;
commaPosition:integer;
begin
tmpStr := Edit1.Text;
commaPosition := pos(',',tmpStr);
tmpStr := copy(tmpStr,1,commaPosition-1);
Edit1.Text := tmpStr;
end;
I'm not a Delphi-Programmer (any more). However, I guess you get the String from the Text-Property of your TEdit-Box object, search for the first occurrence of , and get the index thereof and replace the Text contained in your TEdit-Box by the substring from the beginning of the current string to the found index.
edit.Text := Copy(edit.Text, 1, Pos(',', edit.Text)-1);
Sources:
http://docwiki.embarcadero.com/Libraries/en/System.Copy
http://docwiki.embarcadero.com/Libraries/en/System.Pos
TEdit.Text is a property and cannot be passed as var parameter. But once you introduce temporary variable, you can delegate checking of character index returned from Pos to Delete and it will handle all of cases.
var
S: string;
begin
S := Edit1.Text; // try '955-986, total = 32' and '955-986; total = 32'
Delete(S, Pos(',', S), MaxInt);
Edit1.Text := S;
end;
I have some cases in my code where I am building a large string of text, such as a complex SQL statement. I intend to put this text together many times in a row, each with some slightly different parameters. I've come to the habit of using a subroutine named just procedure A(const S: String); which simply appends the text (S) to the larger string Text := Text + S + #10 + #13;
I was wondering if this could hinder the performance as opposed to using traditional string concatenation? I am beginning to think the compiler optimizes something like this:
Text := 'some' + ' ' + 'text' + ' ' + 'and' + ' ' + 'such';
to
Text := 'some text and such';
Is this true? Does the compiler optimize this scenario? If so, I may decide to change everything to something like this:
Text := 'select something from sometable st'+#10+#13+
'join someothertable sot on sot.id = st.sotid'+#10+#13+
'where sot.somevalue = 1'+#10+#13+
'order by sot.sorting';
Would this be faster theoretically than
Text:= Text + 'select something from sometable st'+#10+#13;
Text:= Text + 'join someothertable sot on sot.id = st.sotid'+#10+#13;
Text:= Text + 'where sot.somevalue = 1'+#10+#13;
Text:= Text + 'order by sot.sorting';
or how I usually do it:
A('select something from sometable st');
A('join someothertable sot on sot.id = st.sotid');
A('where sot.somevalue = 1');
A('order by sot.sorting');
An expression like
'a' + 'b'
is evaluated at compile time. Which means that an assignment
str := 'a' + 'b';
results in identical compiled code to
str := 'ab';
On the other hand, for
str := 'a';
str := str + 'b';
the concatenation is performed at runtime.
Note that putting all concatenations in one expression is still more efficient when non-constant expressions are used.
Consider this code:
A := '*';
B := 'person';
C := 'first_name=''Jerry''';
Q := 'select ';
Q := Q + A;
Q := Q + ' from ';
Q := Q + B;
Q := Q + ' where ';
Q := Q + C;
The six statements above will perform 5 separate concatenations.
Whereas:
Q := 'select ' + A + ' from ' + B + ' where ' + C;
will perform a single concatenation. Delphi will allocate the necessary space for the result and copy each of the six values into that space.