How to implement a decision matrix in Delphi - algorithm

I have to implement a decision matrix in Delphi 7.
The function is
CalcNewStatus( actionCode: string; reportType: string; currentStatus: string): string;
ActionCode can have the values 'A' and 'N'
ReportType can have the values 'I' and 'F'
CurrentStatus can have the values 'P', 'I', 'F'.
In C# I would use a dictionary, for example. How can I do this in Delphi 7?

Normalize your input characters to zero-based ordinal values and things become much easier. Start with a few type declarations:
type
TActionCode = (acA, acN);
TReportType = (rtI, rtF);
TStatus = (sP, sI, sF);
Then you can define an array using those types with all possible status values. Replace sX with whichever status value belongs in each spot.
const
NextStatus: array[TActionCode, TReportType, TStatus] of TStatus = (
{acA} (// sP, sI, sF
{rtI} ( sX, sX, sX),
{rtF} ( sX, sX, sX)
),
{acN} (
{rtI} ( sX, sX, sX),
{rtF} ( sX, sX, sX)
)
);
Then your function is simply this:
function CalcNewStatus(const actionCode, reportType, currentStatus: string): string;
var
ac: TActionCode;
rt: TReportType;
s: TStatus;
const
StateChars: array[TState] of Char = ('P', 'I', 'F');
begin
Assert(actionCode <> ''); Assert(reportType <> ''); Assert(currentStatus <> '');
Assert(actionCode[1] in ['A', 'N']);
Assert(reportType[1] in ['I', 'F']);
Assert(currentStatus[1] in ['P', 'I', 'F']);
if actionCode[1] = 'A' then ac := acA else ac := acN;
if reportType[1] = 'I' then rt := rtI else rt := rtF;
if currentStatus[1] = 'P' then s := sP
else if currentStatus[1] = 'I' then s := sI
else s := sF;
Result := StateChars[NextStatus[ac, rt, s]];
end;
As you can see, most of this code is spent converting between strings and the enum types. If you can, avoid strings in this case. Switch to the enum types as early in your program as possible, and only convert back to strings or characters when you absolutely need to. Strings can have arbitrary length, which you really shouldn't have to deal with, and strings can also have values outside the range you've defined. Enums can't, unless you do something weird. Furthermore, the compiler won't let you accidentally use a TState value where a TReportType is expected, which will help you from confusing your I's and F's.

First of all in such a limited case (2 ActionCodes, 2 ReportTypes, 3 Statuses) I should definitely use enumerated types instead of strings.
And for the decision matrix ... a matrix:
Type
TActionCode = (taA, taN);
TReprotType = (rtI, rtF);
TStatus = (stP, stI, stF);
const
NewStatus: array [TActionCode, TReportType, TStatus] of TStatus =
((((,,)),((,,))),(((,,)),((,,)))) // values of the new statuses here

There is one solution that uses a single-dimensional array.
unit XnResultStatusOverride;
interface
function XnCalcOverridenResultStatus(
actionCodeStr: string;
reportTypeStr: string;
currentStatusStr: string ): string;
implementation
uses SysUtils;
type
TActionCodes = ( ActionCodeA = 0, ActionCodeN = 1);
TReportTypes = (ReportTypeI = 0, ReportTypeF = 1);
TResultStatus = (ResultStatusP = 0, ResultStatusF = 1, ResultStatusI = 2);
const
DecisionMatrix: array[ 0 .. 15 ] of TResultStatus
=
(
ResultStatusF, // 0 A-I-P
ResultStatusF, // 1 A-I-F
ResultStatusF, // 2 A-I-I
ResultStatusF, // 3 N/A
ResultStatusP, // 4 A-F-P
ResultStatusP, // 5 A-F-F
ResultStatusF, // 6 A-F-I
ResultStatusF, // 7 N/A
ResultStatusF, // 8 N-I-P
ResultStatusF, // 9 N-I-F
ResultStatusP, // 10 N-I-I
ResultStatusF, // 11 N/A
ResultStatusF, // 12 N-F-P
ResultStatusI, // 13 N-F-F
ResultStatusF, // 14 N-F-I
ResultStatusF // 15 N/A
);
function ParseActionCodeString( value: string ): TActionCodes;
begin
if value = 'A' then
begin
result := ActionCodeA;
exit;
end;
if value = 'N' then
begin
result := ActionCodeN;
exit;
end;
raise Exception.Create('Invalid action code string' );
end;
function ParseReportTypeString( value: string ): TReportTypes;
begin
if value = 'I' then
begin
result := ReportTypeI;
exit;
end;
if value = 'F' then
begin
result := ReportTypeF;
exit;
end;
raise Exception.Create('Invalid report type string' );
end;
function ParseResultStatusString( value: string ): TResultStatus;
begin
if value = 'P' then
begin
result := ResultStatusP;
exit;
end;
if value = 'F' then
begin
result := ResultStatusF;
exit;
end;
if value = 'I' then
begin
result := ResultStatusI;
exit;
end;
raise Exception.Create('Invalid result status string' );
end;
function ResultStatusToString( value: TResultStatus ): string;
begin
if value = ResultStatusP then
begin
result := 'P';
exit;
end;
if value = ResultStatusF then
begin
result := 'F';
exit;
end;
if value = ResultStatusI then
begin
result := 'I';
exit;
end;
raise Exception.Create('Unknown TResultStatus enum member' );
end;
function XnCalcOverridenResultStatus(
actionCodeStr: string;
reportTypeStr: string;
currentStatusStr: string ): string;
var
actionCode: TActionCodes;
reportType: TReportTypes;
currentStatus:TResultStatus;
discriminant: integer;
newStatusInt: integer;
newStatus: TResultStatus;
begin
actionCode := ParseActionCodeString( actionCodeStr );
reportType := ParseReportTypeString( reportTypeStr );
currentStatus := ParseResultStatusString( currentStatusStr );
discriminant := integer(actionCode) * 8 + integer(reportType) * 4 + integer(currentStatus);
newStatusInt := DecisionMatrix[ discriminant ];
newStatus := TResultStatus( newStatusInt);
Result := ResultStatusToString( newStatus );
end;
end.

Related

How to Validate InputQuery user input before the user clicks OK?

In a 32-bit VCL Application in Windows 10 in Delphi 11 Alexandria, I show an INPUT DIALOG to the user:
var aNewFolderName: string := 'New Project Folder';
if Vcl.Dialogs.InputQuery('New Project Folder', 'Enter the name of the new Project Folder:', aNewFolderName) then
begin
// Todo: Create the folder if everything went OK, ELSE REPEAT the input action :-(
end;
Is there a way to VALIDATE the user's input BEFORE he clicks the OK button? (E.g., checking for not allowed characters, existing folder, etc.). Just repeating the whole input action in the case of invalid input AFTER the user clicked OK is not very smart and efficient:
var aNewFolderName: string := 'New Project Folder';
var InputIsValid: Boolean;
repeat
if Vcl.Dialogs.InputQuery('New Project Folder', 'Enter the name of the new Project Folder:', aNewFolderName) then
begin
InputIsValid := CheckInput(aNewFolderName);
if InputIsValid then CreateTheFolder(aNewFolderName);
end
else
BREAK;
until InputIsValid;
Also, with this method, there is no feedback for the user about the specific cause of any invalid input.
Although it is possible to solve this problem by using repeated dialogs, I don't think that is a particularly elegant solution from a UX perspective.
I'd rather make my own dialog and do something like this:
procedure TForm1.btnSetPasswordClick(Sender: TObject);
begin
var psw := '';
if SuperInput(
Self,
'Frog Simulator',
'Please enter the new frog password:',
psw,
function(const Text: string; out AErrorMessage: string): Boolean
begin
if Text.Length < 8 then
begin
AErrorMessage := 'The password''s length must be at least 8 characters.';
Exit(False);
end;
if not StrHasChrOfType(Text, TCharacter.IsLetter) then
begin
AErrorMessage := 'The password must contain at least one letter.';
Exit(False);
end;
if not StrHasChrOfType(Text, TCharacter.IsDigit) then
begin
AErrorMessage := 'The password must contain at least one digit.';
Exit(False);
end;
if not StrHasChrOfType(Text, TCharacter.IsPunctuation) then
begin
AErrorMessage := 'The password must contain at least one punctuation character.';
Exit(False);
end;
Result := True;
end)
then
lblNewPassword.Caption := psw;
end;
Here's the code:
unit SuperInputDlg;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;
type
TValidator = reference to function(const Text: string;
out AErrorMessage: string): Boolean;
TSuperInputForm = class(TForm)
lblCaption: TLabel;
shClient: TShape;
Edit: TEdit;
pbErrorIcon: TPaintBox;
lblError: TLabel;
Validator: TTimer;
btnOK: TButton;
btnCancel: TButton;
procedure FormCreate(Sender: TObject);
procedure pbErrorIconPaint(Sender: TObject);
procedure EditChange(Sender: TObject);
procedure ValidatorTimer(Sender: TObject);
procedure btnOKClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FErrorIcon: HICON;
FLIWSD: Boolean;
FValidator: TValidator;
function DoValidate: Boolean;
public
end;
function SuperInput(AOwnerForm: TCustomForm; const ACaption, AMainInstruction: string;
var AText: string; AValidator: TValidator = nil): Boolean;
implementation
{$R *.dfm}
function Scale(X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, 96);
end;
procedure TSuperInputForm.btnOKClick(Sender: TObject);
begin
if DoValidate then
ModalResult := mrOK;
end;
function TSuperInputForm.DoValidate: Boolean;
begin
var LErrMsg: string;
var LIsValid := not Assigned(FValidator) or FValidator(Edit.Text, LErrMsg);
btnOK.Enabled := LIsValid;
if not LIsValid then
lblError.Caption := LErrMsg;
pbErrorIcon.Visible := not LIsValid;
lblError.Visible := not LIsValid;
Result := LIsValid;
end;
procedure TSuperInputForm.EditChange(Sender: TObject);
begin
Validator.Enabled := False;
Validator.Enabled := True;
end;
procedure TSuperInputForm.FormCreate(Sender: TObject);
var
ComCtl: HMODULE;
LoadIconWithScaleDown: function(hinst: HINST; pszName: LPCWSTR; cx: Integer;
cy: Integer; var phico: HICON): HResult; stdcall;
begin
ComCtl := LoadLibrary('ComCtl32.dll');
if ComCtl <> 0 then
begin
try
LoadIconWithScaleDown := GetProcAddress(ComCtl, 'LoadIconWithScaleDown');
if Assigned(LoadIconWithScaleDown) then
LoadIconWithScaleDown(0, IDI_ERROR, Scale(16), Scale(16), FErrorIcon);
finally
FreeLibrary(ComCtl);
end;
end;
FLIWSD := FErrorIcon <> 0;
if FErrorIcon = 0 then
FErrorIcon := LoadIcon(0, IDI_ERROR);
end;
procedure TSuperInputForm.FormDestroy(Sender: TObject);
begin
if FLIWSD then
DestroyIcon(FErrorIcon);
end;
procedure TSuperInputForm.pbErrorIconPaint(Sender: TObject);
begin
if FErrorIcon <> 0 then
DrawIconEx(pbErrorIcon.Canvas.Handle, 0, 0, FErrorIcon,
Scale(16), Scale(16), 0, 0, DI_NORMAL);
end;
procedure TSuperInputForm.ValidatorTimer(Sender: TObject);
begin
DoValidate;
end;
function SuperInput(AOwnerForm: TCustomForm; const ACaption, AMainInstruction: string;
var AText: string; AValidator: TValidator = nil): Boolean;
begin
var LFrm := TSuperInputForm.Create(AOwnerForm);
try
LFrm.Caption := ACaption;
LFrm.lblCaption.Caption := AMainInstruction;
LFrm.Edit.Text := AText;
LFrm.FValidator := AValidator;
LFrm.DoValidate;
Result := LFrm.ShowModal = mrOk;
if Result then
AText := LFrm.Edit.Text;
finally
LFrm.Free;
end;
end;
end.
and DFM:
object SuperInputForm: TSuperInputForm
Left = 0
Top = 0
Caption = 'Input Box'
ClientHeight = 166
ClientWidth = 469
Color = clBtnFace
Constraints.MaxHeight = 204
Constraints.MinHeight = 204
Constraints.MinWidth = 400
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
OldCreateOrder = False
Position = poOwnerFormCenter
OnCreate = FormCreate
OnDestroy = FormDestroy
DesignSize = (
469
166)
PixelsPerInch = 96
TextHeight = 15
object shClient: TShape
Left = 0
Top = 0
Width = 468
Height = 127
Anchors = [akLeft, akTop, akRight, akBottom]
Pen.Style = psClear
ExplicitWidth = 499
ExplicitHeight = 175
end
object lblCaption: TLabel
Left = 24
Top = 24
Width = 65
Height = 21
Caption = 'Input Box'
Font.Charset = DEFAULT_CHARSET
Font.Color = 10040064
Font.Height = -16
Font.Name = 'Segoe UI'
Font.Style = []
ParentFont = False
end
object pbErrorIcon: TPaintBox
Left = 24
Top = 88
Width = 16
Height = 16
OnPaint = pbErrorIconPaint
end
object lblError: TLabel
Left = 50
Top = 88
Width = 3
Height = 15
end
object Edit: TEdit
Left = 24
Top = 51
Width = 418
Height = 23
Anchors = [akLeft, akTop, akRight]
TabOrder = 0
OnChange = EditChange
ExplicitWidth = 449
end
object btnOK: TButton
Left = 286
Top = 133
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Caption = 'OK'
Default = True
TabOrder = 1
OnClick = btnOKClick
ExplicitLeft = 317
ExplicitTop = 181
end
object btnCancel: TButton
Left = 367
Top = 133
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Cancel = True
Caption = 'Cancel'
ModalResult = 2
TabOrder = 2
ExplicitLeft = 398
ExplicitTop = 181
end
object Validator: TTimer
OnTimer = ValidatorTimer
Left = 136
Top = 120
end
end
Please note that this is only a sketch I did in ten minutes -- in a real app, you'd spend a bit more time on this one.
Appendix 1
type
TChrTestFcn = function(C: Char): Boolean;
function StrHasChrOfType(const AText: string; ATestFcn: TChrTestFcn): Boolean;
begin
for var S in AText do
if ATestFcn(S) then
Exit(True);
Result := False;
end;
No, there is no way. But there are alternatives:
Look up InputQuery's code, write similar code and then make modifications where you want them (e.g. setting an OnChange handler for the TEdit). You'll notice it has been done in a quite simple way.
Design your own form for it, instead of (like InputQuery does) creating one on the fly:
You can either use your own logic for getting the user's choice and input when the form is closed, or
you set the form's ModalResult property (via code) anytime, and you can assign a ModalResult to each button (e.g. mrCancel), so it automatically sets the form's ModalResult property. Call your own form just as if it's MessageBox():
case MyForm.ShowModal() of
IDOK: begin
// User wants to proceed AND no error was found
end;
IDCANCEL: begin
// User gave up
end;
else
// Unexpected result
end;
For me it would make nearly no difference if any feedback is given with any form still visible or not, as long as it is given. And you can give it, because nothing keeps you from doing so:
var
aNewFolderName,
sErrorMsg: String;
begin
repeat
aNewFolderName:= 'New Project Folder';
// User cancelled: leaving loop
if not Vcl.Dialogs.InputQuery( 'New project folder', 'Enter the name of the new project folder:', aNewFolderName ) then break;
// Already errors before the attempt?
sErrorMsg:= CheckInput( aNewFolderName );
// Actual attempt, which may still go wrong for various reasons
if sErrorMsg= '' then sErrorMsg:= CreateTheFolder( aNewFolderName );
// Anything to report?
if sErrorMsg<> '' then begin
sErrorMsg:= sErrorMsg+ #13#10
+ 'Click "Retry" to try a different new project name.'+ #13#10
+ 'Click "Cancel" to not create any new project.';
if MessageBox( sErrorMsg, 'Error', MB_RETRYCANCEL )= IDCANCEL then break;
end else begin
break; // Otherwise we're done (CreateTheFolder was successful)
end;
until FALSE;
end;
The easiest and most versatile option is to write your own input dialog and either include the validation code in the dialog or alternatively pass a callback method that does the validation.
But to answer the question as you asked it:
"Is there a way to VALIDATE the user's input BEFORE he clicks the OK button?"
No, there is no way, as far as I know.
Thanks to the users who explicitly confirmed that there is no predesigned way to validate the input before the user clicks OK. Now the remaining question is: How to provide a meaningful and efficient feedback about the invalidation cause to the user? IMO, showing an ADDITIONAL error dialog does not meet these criteria. So I implemented a solution where the prompt is replaced by the validation error message in the reappearing InputQuery dialog:
procedure TformMain.menuitemCreateASubProjectGroupClick(Sender: TObject);
begin
var aNewFolderName: string := 'New Project Group';
var InputIsValid: Boolean;
var InputPrompt: string := 'Enter the name of the new Project Group:';
repeat
if Vcl.Dialogs.InputQuery('New Project Group', InputPrompt, aNewFolderName) then
begin
var NewPrompt: string;
InputIsValid := CheckInput(aNewFolderName, NewPrompt);
if not InputIsValid then
InputPrompt := NewPrompt
else
begin
CreateNewFolder(aNewFolderName);
end;
end
else
BREAK;
until InputIsValid;
end;
function PAIsFilenameValid(const AFilename: string; var errorchar: Char): Boolean;
begin
Result := True;
for var i := 1 to Length(AFilename) do
begin
if not System.IOUtils.TPath.IsValidFileNameChar(AFilename[i]) then
begin
errorchar := AFilename[i];
Result := False;
BREAK;
end;
end;
end;
function TformMain.CheckInput(const aNewFolderName: string; out aNewPrompt: string): Boolean;
begin
Result := True;
var errorchar: Char;
if Trim(aNewFolderName) = '' then
begin
aNewPrompt := 'Error: You must enter a Group name:';
Result := False;
end
else if not PAIsFilenameValid(aNewFolderName, errorchar) then
begin
aNewPrompt := 'Error: No illegal characters (' + errorchar + '):';
Result := False;
end;
// Todo: else...
end;
Example screenshots:

indy gettickdiff64() 18446744073709551600 problem

I found that the gettickdiff64 function sometimes results in 18446744073709551600 (or 18446744073709551601) and causes the
program to run incorrectly.
Normally does not have a result greater than 300000
what might this be about?
Should I always do extra checks against this problem?
it is 32 bit VCL application.
I use Delphi 10.4.1( its indy version 10.6.2.0 )
Running on: 64 bit Windows Server 2012 R2 Foundation / intel xeon cpu E3-1225 v5 3.3 Ghz.
The code structure is as follows:
TMyClass = class
private
//.............
lastSetTime: uint64;
critic: TCriticalSection;
public
//.............
procedure setLastSetTime( ltime: uint64 );
function getLastSetTime: uint64;
end;
procedure TMyClass.setLastSetTime( ltime: uint64 );
begin
critic.enter;
try
lastSetTime := ltime;
finally
critic.leave;
end;
end;
function TMyClass.getLastSetTime: uint64;
begin
critic.enter;
try
result := lastSetTime;
finally
critic.leave;
end;
end;
...........
procedure controlAll(); //------>this is called from within thread every 5 minutes
var oki: boolean;
starttime, tdiff, ltime: uint64;
i: integer;
myC, sC: TMyClass;
begin
oki := false;
starttime := ticks64();
while ( oki = false ) and ( gettickdiff64( starttime, ticks64 ) < 40000 ) do
begin
//.........
//.........
sC := nil;
with myClassList.LockList do
try
if count > 0 then //---> has about 50000
begin
i := 0;
while i < count do
begin
myC := TMyClass( items[ i ] );
ltime := myC.getLastSetTime();
tdiff := gettickdiff64( ltime, ticks64() );
if tdiff > 50000 then
begin
logToFile( tdiff.ToString + ' ' + ltime.ToString ); //-----> every 5 minutes 50-60 log lines occur like this: 18446744073709551600 468528329
//..........
//.........
sC := myC;
delete( i );
break;
end;
inc( i );
end;
end;
finally
myClassList.UnlockList;
end;
if sC = nil then oki := true
else
begin
//..........
//..........
end;
end;
end;
The code structure that sets this value is as follows.
classListArray keeps all classes of TMyClass type grouped by server and channel number.
myClassList keeps all classes of type TMyClass attached one after the other without grouping.
classListArray is used to spend less CPU and process faster.
These two lists are not protected against each other when accessing classes.
Protection against each other is done only when adding and deleting classes.
classListArray: array[ 1..250, 1..12 ] of TThreadList;
//.................
procedure ServerExecute(AContext: TIdContext);
var Ath: TMypeer;
severNum, channelNum, clientNum, i, j, num: integer;
pSize: word;
stream: Tmemorystream;
packageNum: byte;
begin
try
Ath := TMypeer( AContext );
serverNum := Ath.getServerNum();
channelNum := Ath.getChannelNum();
Ath.SendQueue();
if AContext.Connection.IOHandler.InputBufferIsEmpty then
if not AContext.Connection.IOHandler.CheckForDataOnSource( 50 ) then Exit;
clientNum := AContext.Connection.IOHandler.ReadInt32( false );
pSize := AContext.Connection.IOHandler.ReadUInt16( false );
stream := TMemorystream.create;
try
AContext.Connection.IOHandler.ReadStream( stream, pSize );
stream.Seek( 0, soFromBeginning );
if clientNum <> 0 then
begin
//...........
end
else
begin
stream.ReadBuffer( packageNum, sizeof( packageNum ) );
if packageNum = 10 then
begin
stream.ReadBuffer( num, sizeof( num ) );
for i := 1 to num do
begin
stream.ReadBuffer( clientNum, sizeof( clientNum ) );
with classListArray[ serverNum, channelNum ].LockList do
try
if count > 0 then
for j := 0 to count - 1 do
begin
if TMyClass( items[ j ] ).getClientNum = clientNum then
begin
TMyClass( items[ j ] ).setLastSetTime( ticks64 ); //**********
break;
end;
end;
finally
classListArray[ serverNum, channelNum ].unLockList;
end;
end;
end
else
//.........
end;
finally
stream.free;
end;
except on e:exception do
begin
if E is Eidexception then raise
else
begin
logToFile( e.message );
//..........
end;
end;
end;
end;
According to your log, ltime was 468528329 and GetTickDiff64(ltime, Ticks64()) returned 18446744073709551600. Given the simple implementation of GetTickDiff64() (where TIdTicks is UInt64):
function GetTickDiff64(const AOldTickCount, ANewTickCount: TIdTicks): TIdTicks;
{$IFDEF USE_INLINE}inline;{$ENDIF}
begin
{This is just in case the TickCount rolled back to zero}
if ANewTickCount >= AOldTickCount then begin
Result := TIdTicks(ANewTickCount - AOldTickCount);
end else begin
Result := TIdTicks(((High(TIdTicks) - AOldTickCount) + ANewTickCount) + 1);
end;
end;
The only way this code can return 18446744073709551600 given AOldTickCount=468528329 is if ANewTickCount is either 18446744074178079929 or 468528313.
Since VCL runs on Windows only, and on Windows Ticks64() is just a thin wrapper around the Win32 GetTickCount64() function on Vista and later, it is very unlikely that Windows would ever produce such an astronomically large number like 18446744074178079929 for the current tick counter (that would be 213503982340 days from bootup). So it must have returned 468528313 instead, which is more reasonable (that is just 5.4 days from bootup). That is 16ms less than ltime=468528329, so GetTickDiff64() would assume that Windows' tick counter had exceeded High(UInt64) and wrapped back around to 0 (which is unlikely for a 64-bit tick counter to ever do in our lifetime).
So, you need to debug your code and figure out how Ticks64()/Windows could possibly return 468528329 and then later return 468528313. I suspect it is really not doing that, and that there is more likely a bug in your code that we can't see which is storing the wrong value into TMyClass.lastSetTime to begin with.
That being said, you might consider getting rid of the overhead of TCriticalSection and use TInterlocked instead to read/write your UInt64 member atomically.
Or, try using Delphi's own TStopWatch instead of tracking ticks manually.

Pascal Segmentation Fault parsing Text File

I am working on a Question/Answer UI application in Pascal / Lazarus. My problem is that upon invoking below code through a button click, the program crashes with a Segmentation Fault error.
// more declarations... (UI Form, Buttons, ...)
type
TQuestion = class(TObject)
title: string;
answers: array of string;
correct: integer;
end;
var
questions: array of TQuestion;
procedure TForm1.BStartClick(Sender: TObject);
var
i: integer;
j: integer;
line: string;
arrayLength: integer;
question: TQuestion;
stringList: TStringList;
begin
stringList := TStringList.create;
stringList.LoadFromFile('questions.txt');
for i := 0 to stringList.Count - 1 do ;
begin
line := stringList[i];
if (length(line) >= 2) then
if (line[2] = ' ') and ((line[1] = '-') or (line[1] = '+')) then
begin
arrayLength := length(question.answers);
SetLength(question.answers, arrayLength + 1);
question.answers[arrayLength] :=
Copy(line, 2, Length(line) - 1);
if zeile[1] = '+' then
question.correct := arrayLength;
end
else
begin
question := TQuestion.Create;
question.title := line;
arrayLength := length(questions);
setLength(questions, arrayLength + 1);
questions[arrayLength] := question;
end;
end;
BStart.Visible := False;
end;
Well, my Pascal knowledge goes to 10 to 15 years ago. However, I can see that you have an extra semicolon at the end of this line:
for i := 0 to stringList.Count - 1 do ;

Lazarus display numbers from memo to for exampel lisbox

I have these data:
CMD210 STA_ 99.0 uS Temp 22.1 C
CMD210 STAB 99.9 uS Temp 22 C
CMD210 STAB 0.1 mS Temp 22.1 C
CMD210 STA_ 99.5 uS Temp 22.1 C
CMD210 STAB 99.4 uS Temp 22 C
CMD210 ST__ 99.0 uS Temp 22.2 C
CMD210 STAB 0.1 mS Temp 22 C
CMD210 STAB 99.3 uS Temp 22.2 C
I would like to have a program that display the temperature from memo for exampel in a listbox.
I know I have to get loop and something with 2 char with 'p' and 'c', because the number is between those to letters....
procedure TForm1.Button4Click(Sender: TObject);
var
midlet,midler:char;
resultat,x:integer;
linecount,index:integer;
found: boolean;
begin
midlet:= 'p';
midler:='C';
index:=0;
resultat:=midlet+x+midler
found := false;
linecount := Memo1.lines.count;
while index<= linecount - 1 do
begin
if x = memo1.lines[index] then
found := true;
index :=index + 1;
end
if found = true then
ListBox1.text:= floattostrF(x,ffFixed,15,2);
end;
There are several problems in your example so this answer will be limited to "how extracting and converting the temperature from a line". You have fundamentally two ways to achieve the task:
use the regular expressions.
write a custom parser.
the custom parser is quite easy to write:
accumulate non-blank chars in an identifier.
if the identifier is equal to Temp then define a flag.
convert the identifier to a double if the flag is defined and if someting's been accumulated.
example:
program Project1;
uses
sysutils;
const
line1 = 'CMD210 STAB 99.3 uS Temp 22.2 C';
line2 = 'CMD210 STAB 0.1 mS Temp 22 C';
line3 = 'it is quite hot over there Temp 55.123456 C';
line4 = 'bla bla bla bla 12.564 C';
line5 = '';
function getTemperature(aLine: string): double;
var
reader: PChar;
identifier: string;
AccumulateTemp: boolean;
const
_Nan = 0/0;
begin
// initialize local variables.
identifier := '';
AccumulateTemp := false;
// success can be tested with isNan()
result := _Nan;
// add a distinct terminal char:
aLine := aLine + #0;
reader := #aLine[1];
while(true) do begin
if reader^= #0 then
exit;
// blank: test the identifier
if reader^ in [#9, ' '] then
begin
if AccumulateTemp then
begin
if not TryStrToFloat(identifier, result) then
result := _Nan;
AccumulateTemp := false;
exit;
end;
if identifier = 'Temp' then
AccumulateTemp := true;
identifier := '';
end else
// accumulate
identifier := identifier + reader^;
Inc(reader);
end;
end;
begin
DecimalSeparator := '.';
writeln( format('%.7f', [getTemperature(line1)]) );
writeln( format('%.7f', [getTemperature(line2)]) );
writeln( format('%.7f', [getTemperature(line3)]) );
writeln( format('%.7f', [getTemperature(line4)]) );
writeln( format('%.7f', [getTemperature(line5)]) );
readln;
end.
which outputs
22.2000000
22.0000000
55.1234560
Nan
Nan

Given a recordset, how to populate a TDataSet descendant, in order to populate a grid later?

Currently I have followed the example present in the documentation.
Specifically the example named:
Example: Stored Procedure Returning Multiple Rowsets
This is the delphi code translated from the above example:
//Copied from ADODB.pas
function CreateADOObject(const ClassID: TGUID): IUnknown;
var
Status: HResult;
FPUControlWord: Word;
begin
asm
FNSTCW FPUControlWord
end;
Status := CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IUnknown, Result);
asm
FNCLEX
FLDCW FPUControlWord
end;
if (Status = REGDB_E_CLASSNOTREG) then
raise Exception.CreateRes(#SADOCreateError) else
OleCheck(Status);
end;
var
Con: TADOConnection;
RSet: _Recordset;
Cmd: _Command;
P1, P2: _Parameter;
i: integer;
RecordsAffected: OleVariant;
begin
Con := TADOConnection.Create(nil);
try
Con.ConnectionString := PromptDataSource(Handle, '');
Con.Open('user', 'password');
Cmd := CreateADOObject(CLASS_Command) as _Command;
try
Cmd.Set_ActiveConnection(Con.ConnectionObject);
P1 := Cmd.CreateParameter('P1', adSmallInt, adParamInput, 0, 1);
Cmd.Parameters.Append(P1);
P2 := Cmd.CreateParameter('P2', adSmallInt, adParamOutput, 0, EmptyParam);
Cmd.Parameters.Append(P2);
Cmd.Properties['PLSQLRSet'].Value := True;
Cmd.CommandType := adCmdText;
Cmd.CommandText := '{CALL Employees.GetEmpRecords(?, ?)}';
RSet:= CreateADOObject(CLASS_Recordset) as _RecordSet;
try
//If I use Execute, the CursorLocation will be adUseServer
RSet.Open(Cmd, EmptyParam, adUseClient, adOpenStatic, adCmdText);
ShowMessage(IntToStr(RSet.RecordCount));
for I:= 0 to RSet.Fields.Count-1 do
begin
showmessage(vartostr(RSet.Fields.Item[i].Name)+': '+vartostr(RSet.Fields.Item[i].Value));
end;
Cmd.Properties['PLSQLRSet'].Value := False;
finally
RSet := nil;
end;
finally
Cmd := nil;
end;
finally
Con.Free;
end;
end;
NOTE: I changed the procedure to return only one cursor!
Question:
1.a) How Can I fill a DataSet from the recordset?
Or
1.b) Is there another way to capture out cursor parameters from an oracle procedure through ADO in Delphi?
UPDATE 1:
As suggested by MartynA I tried to use TADQuery, but unsuccessfully so far:
object ADOQuery1: TADOQuery
Connection = ADOConnection1
CursorType = ctStatic
Parameters = <
item
Name = 'param0'
DataType = ftUnknown
Direction = pdOutput
Size = -1
Value = Null
end
item
Name = 'param1'
DataType = ftSmallint
Size = -1
Value = 7084
end
item
Name = 'param2'
DataType = ftSmallint
Direction = pdOutput
Size = -1
Value = Null
end>
SQL.Strings = (
'{CALL Employees.GetEmpRecords(:param0, :param1, :param2)}')
Left = 560
Top = 96
end
procedure TForm1.ADOConnection1WillExecute(Connection: TADOConnection;
var CommandText: WideString; var CursorType: TCursorType;
var LockType: TADOLockType; var CommandType: TCommandType;
var ExecuteOptions: TExecuteOptions; var EventStatus: TEventStatus;
const Command: _Command; const Recordset: _Recordset);
begin
CommandType := cmdText;
Command.Properties['PLSQLRSet'].Value := True;
end;
You can assign directly your recordset to an ADODataset
ADODataset := TADODataset.Create(nil);
ADODataset.Recordset := RecordsetData;
It is very simple to answer the first :
RSet.Open(Cmd, EmptyParam, adUseClient, adOpenStatic, adCmdText);
DataSet := TADOQuery.Create(nil);
try
DataSet.Recordset := RSet;
...
When you 'SET' the RecordSet into the DataSet, it is automatically filled with recordset data.

Resources