How to output an array that has nested arrays in Pascal - arrays

I'm creating a basic concept of a music player using Pascal, but I'm struggling to display the albums inside it. The error I got says "(134, 29) Error: Can't read or write variables of this type". I'm assuming it's saying that because I'm using an array within an array, and it's having a hard time displaying both at the same time (although I only want it to display the albums, not the tracks as well).
Here's what my code looks like:
function ReadAllTrack(prompt: String): Tracks;
var
i: Integer;
trackArray: Array of Track;
trackCount: Integer;
begin
WriteLn(prompt);
trackCount := ReadIntegerGreaterThan1('Please enter the number of tracks you would like to add: ');
Setlength(trackArray, trackCount);
for i := 0 to trackCount - 1 do
begin
WriteLn('Enter the details for your track:');
trackArray[i] := ReadTrack();
end;
result := trackArray;
end;
function ReadAlbum(): Album;
begin
result.albumName := ReadString('Album name: ');
result.artistName := ReadString('Artist name: ');
result.albumGenre := ReadGenre('Genre:');
result.trackCollection := ReadAllTrack('Track Collection:');
end;
function ReadAllAlbums(): Albums;
var
i: Integer;
albumArray: Array of Album;
albumCount: Integer;
begin
albumCount := ReadIntegerGreaterThan1('Please enter the number of albums you would like to add: ');
Setlength(albumArray, albumCount);
for i := 0 to albumCount - 1 do
begin
WriteLn('Enter the details for your album:');
albumArray[i] := ReadAlbum();
end;
result := albumArray;
end;
procedure DisplayAlbumOptions(listOfAllAlbums: Albums);
var
userInput: Integer;
begin
WriteLn('1. Display all albums');
WriteLn('2. Display all albums for a genre');
userInput := ReadIntegerRange('Please enter a number (1, 2) to select: ', 1, 2);
case userInput of
1: WriteLn(listOfAllAlbums); //Error: Can't read or write variables of this type
end;
end;
Basically what this does is it will ask the user showing 5 options:
1. Add albums
2. Display albums
etc
If the user selects 1, the program will ask the user to input the number of albums they want to input. Then for each album it'll ask them to enter the details, and then the tracks.
Then if the user selects 2, the program will ask the user to choose either display every single album there is, or display all albums for a single genre (I'll be working on this one after solving this problem). At first I thought it would be just as simple as WriteLn(TheAlbumArray); but turns out it was more complicated than I thought because I don't think it's possible for the program to display it this way. I tried separating the albums and tracks so that it would only display the albums when I use WriteLn(TheAlbumArray); but it wasn't possible because the tracks still have to be "inside" the album so that when I display the albums and select one of them, it would then display the tracks....
Any help or suggestion for this and/or the second will be much appreciated ^^

Your original question contained a lot of superfluous detail. After the edit, you removed the type declarations, but kept much of the superfluous detail.
However, it is possible to discern the problem you are passing an array of record to Writeln. The Writeln function can accept only certain simple types as arguments, e.g. strings, numerical types, boolean. You certainly cannot pass an array to Writeln. You must iterate over the array and process each member individually.
So you might try
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
WriteLn(listOfAllAlbums[i]);
But that does not work either, because listOfAllAlbums[i] is a record, and a record is a compound type which cannot be passed to Writeln. So you need to process the record separately. If you want to display just the title, then you write:
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
WriteLn(listOfAllAlbums[i].albumName);
If you want to print the track titles too then you need to iterate over the array contained in the record.
for i := low(listOfAllAlbums) to high(listOfAllAlbums) do
begin
WriteLn(listOfAllAlbums[i].albumName);
for j := low(trackCollection) to high(trackCollection) do
WriteLn(listOfAllAlbums[i].trackCollection[j]);
end;

It is impossible to use composite types (arrays, records, ...) in Read[ln] and Write[ln] procedures.
To make your code more transparent you could to create type helper for your array(s) and use well-known AsString property. Here is example for simple array of Integer:
program foo;
{$mode objfpc}{$H+}
{$modeswitch typehelpers}
uses
Classes, SysUtils;
type
TMyArray = array of Integer;
TMyArrayHelper = type helper for TMyArray
private
function GetAsString: string;
procedure SetAsString(const AValue: string);
public
property AsString: string read GetAsString write SetAsString;
end;
function TMyArrayHelper.GetAsString: string;
var
i: Integer;
begin
Result := '';
for i in Self do
begin
if Result <> '' then
Result := Result + ', ';
Result := Result + IntToStr(i);
end;
Result := '[' + Result + ']';
end;
// Relatively simple parser
// Fill free to implement ones for your array type
procedure TMyArrayHelper.SetAsString(const AValue: string);
var
tmp, s: string;
items: TStringArray;
i: Integer;
begin
tmp := Trim(AValue);
if not (tmp.StartsWith('[') and tmp.EndsWith(']')) then
raise Exception.CreateFmt('Invalid array literal format: "%s"', [tmp]);
tmp := tmp.Trim(['[', ']']);
items := tmp.Split([',']);
for s in items do
try
StrToInt(s);
except
on e: Exception do
raise Exception.CreateFmt('Invalid integer literal: "%s"', [s]);
end;
SetLength(Self, Length(items));
for i := 0 to Length(items) - 1 do
Self[i] := StrToInt(items[i]);
end;
var
a1, a2: TMyArray;
begin
a1.AsString := '[1,2,3,5]';
Writeln('a1 = ', a1.AsString);
a2.AsString := a1.AsString;
a2[1] := 999;
Writeln('a2 = ', a2.AsString);
end.
Helper types in FreePascal
TStringHelper in SysUtils unit

Related

How do i call a function within a function?

i'm creating a mini program in pascal to read music albums. The problem i'm facing is calling the readTracks() function within my readAlbum() function. Any help would be great, thank you :)
The current error when executing the code is:
Error: Incompatible types: got "albumRecord" expected "albumRecord.Dynamic Array Of trackRecord
type
trackRecord = record
trackName:string;
fileLocation: string;
end;
albumRecord = record
albumName:string;
tracks: array of trackRecord;
end;
function readTracks():albumRecord;
var
i:Integer;
numOfTracks:Integer;
begin
numOfTracks := readInteger('Enter number of tracks: ');
setLength(result.tracks, numOfTracks);
for i:= 0 to high(result.tracks)do
begin
Writeln('Enter Track ',i+1,' Details: ');
result.tracks[i].trackName := readString('Enter track name: ');
result.tracks[i].fileLocation := readString('Enter file
location: ');
end;
end;
function readAlbum (prompt: string): albumRecord;
begin
result.albumName := readString('Enter Album Name: ');
result.tracks := readTracks();
end;
I think you have managed to confuse yourself by the way you have declared your albumRecord.Tracks. What you should have done is to declare a trackArray type and declared your readTracks to return an instance of this array type.
Your main problem was that your readAlbum returns an albumRecord, but you had set up its tracks field to be assigned from readTracks, which returns the wrong type, i.e. albumRecord, rather than an array of tracks.
Put another way, as you have declared readAlbum to return an albumRecord, the compiler will generate code which, on entry to the function, sets up an instance of albumRecord on the stack, which is eventually returned as the return value of the function when it completes. Your code in the function is simply to fill out the fields of this record, not create another instance of it as your result.tracks := readTracks() would have done, given how you had declared readTracks.
The code below changes the declarations and code inside readTracks so that it does what you intend.
uses TerminalUserInput;
type
trackRecord = record
trackName:string;
fileLocation: string;
end;
trackArray = array of trackRecord;
albumRecord = record
albumName:string;
tracks: trackArray;
end;
function readTracks():trackArray;
var
i:Integer;
numOfTracks:Integer;
begin
numOfTracks := readInteger('Enter number of tracks: ');
setLength(result, numOfTracks);
for i:= 0 to high(result)do
begin
Writeln('Enter Track ',i+1,' Details: ');
result[i].trackName := readString('Enter track name: ');
result[i].fileLocation := readString('Enter file location: ');
end;
end;
function readAlbum (prompt: string): albumRecord;
begin
result.albumName := readString('Enter Album Name: ');
result.tracks := readTracks();
end;
begin
readAlbum('New album');
end.
Btw, I think you would find your code much clearer, when you come back to it after a while if you got into the habit of using a naming convention for declarations of record- and array types which distinguishes them from instances of them. One convention is to precede the type-name with a 'T', so your would be TtrackRecord, TalbumRecord, TtrackArray.
Also btw, in your q you say
The current error when executing the code is: Error: Incompatible types
Actually, that is not quite correct. The error occurs while the compiler is compiling the code, not when your code is executing. This is an important difference: your error is known as a compile-time error, whereas one which occurs when your program is executing (which it can only do once it has been successfully compiled) is known as a run-time error.
tracks is an array, and thus needs indexing. Which number are you reading?
In the for loop you do know how to index the array, so why can't you do it in readalbum?

Adding Values to an Array in Pascal - iIllegal Qualifier"

I am trying to create a program that prints 11 buttons so I wanted to use an array. The only change with these buttons is the name.
When I try to compile, I get the error "illegal qualifier" at my first array assignment.
type
buttonName = array[0..11] of String;
procedure PopulateButton(const buttonName);
begin
buttonName[0] := 'Sequence';
buttonName[1] := 'Repetition';
buttonName[2]:= 'Modularisation';
buttonName[3]:= 'Function';
buttonName[4]:= 'Variable';
buttonName[5]:= 'Type';
buttonName[6]:= 'Program';
buttonName[7]:= 'If and case';
buttonName[8]:= 'Procedure';
buttonName[9]:= 'Constant';
buttonName[10]:= 'Array';
buttonName[11]:= 'For, while, repeat';
end;
and in main I am trying to use this for loop
for i:=0 to High(buttonName) do
begin
DrawButton(x, y, buttonName[i]);
y:= y+70;
end;
Please know, I am very new to this and am not too confident of my knowledge in arrays, parameters/calling by constant and such.
Thank you
The parameter definition of PopulateButton() is wrong.
Try this:
type
TButtonNames = array[0..11] of String;
procedure PopulateButtons(var AButtonNames: TButtonNames);
begin
AButtonNames[0] := 'Sequence';
...
end;
...
var lButtonNames: TButtonNames;
PopulateButtons(lButtonNames);
for i := Low(lButtonNames) to High(lButtonNames) do
begin
DrawButton(x, y, lButtonNames[i]);
y:= y+70;
end;
Also pay attention to the naming conventions. Types normally begin with a T and function parameters start with an A.

Delphi - Re-split an array of string?

Let's say I have a string like this :
string1 := 'me,email1,you,email2,him,email3,them,email4';
To turn this into an array of string I simply do :
array1 := SplitString(string1,',');
This works fine.
But then, I get an array like :
array1[0] -> me
array1[1] -> email1
array1[2] -> you
array1[3] -> email2
array1[4] -> him
array1[5] -> email3
array1[6] -> them
array1[7] -> email4
I've searched a long time how to insert into SQLIte with this but there is no using
for i:= 0 to length(array1) -1
SQLExecute('INSERT INTO table(name,email) VALUES("'+array1[i]+'","'+array1[i+1]+'"');
because index 0 will be inserted as name with index 1 as email, but on the next turn, index 1 will be inserted as name with index 2 as email, when index 1 is en email, and index 2 a name... do you see the problem ?
I thought about re-spliting the first array into a second one by changing the initial string format into :
string1 := 'me-email1,you-email2,him-email3,them-email4';
to split a first time on the ' and a second time on the -, to get a 2 dimensional array, but seems this concept is over my knowledge at the moment :)
Just for the record, the Delphi RAD I'm using is quite recent, and only a few functions / tools are available at the moment.
How would you insert into sql ? Would you keep the original String format, or change it to get a 2 dimensional array ?
Iterate in pairs:
for i := 0 to length(array1) div 2 - 1 do
SQLExecute('INSERT INTO table(name,email) VALUES("'+array1[i*2]+'","'+array1[i*2+1]+'"');
You just should not use per-INSERT FOR-loop here.
It is not suited for it and it is dangerous if your string would have 3 or 7 or any other odd number of elements.
Also splicing random data right into the SQL command is extremely unreliable and fragile. http://bobby-tables.com/
You should either use a WHILE-loop with a sliding fetcher or tick-tock (finite state machine) swinging in one-per-string FOR-loop.
var q: TQuery;
// some Query-component of any library,
// including DBX, AnyDAC/ FireDAC mORMot or any other library you would use
var sa_OK, sa_err1, sa_err2: TArray<string>;
// common preparations for both methods
q.ParamCheck := True;
q.SQL.Text := 'INSERT INTO table(name,email) VALUES( :PName, :PMail )';
q.Prepared := True;
sa_OK := TArray<string>.Create( 'me','email1','you','email2','him','email3','them','email4');
// eeeaaasy example
sa_err1 := TArray<string>.Create( 'me','email1','you','email2','him');
// check this out-of-sync error too - it can happen! you should be pre-aware!
sa_err2 := TArray<string>.Create( 'Sarah O''Connor','email1','"Bobby F. Kennedy"','email2','him'#0'again','email3');
// not the letters you expected - but they can come too
Procedure Method1_Fetcher( const sa: array of string );
var i: integer;
s: string;
function Fetched: Boolean;
begin
Result := i <= High(sa);
if Result then begin
s := sa[i];
Inc(i);
end;
end;
Begin
i := Low(sa);
while true do begin
if not Fetched then break;
q.Params[1].AsWideString := s;
if not Fetched then break;
q.Params[2].AsWideString := s;
// ...you can easily add more parameters for more columns
// if not Fetched then break;
// q.Params[3].Value := s;
// ... or you can make a loop
// FOR j := 0 to q.Params.Count - 1 DO ... q.Params[j] :=...
// only executing insert if ALL the columns were filled
q.ExecSQL;
end;
q.Transaction.CommitWork;
End;
Procedure Method2_TickTock( const sa: array of string );
var i, FSM: integer;
Begin
FSM := 0;
for i := Low(sa) to High(sa) do begin
if FSM = 0 then begin
q.ParamByName('PName').AsWideString := sa[i];
FSM := 1; // next mode of tick-tock
Continue;
end;
if FSM = 1 then begin
q.ParamByName('PMail').AsWideString := sa[i];
q.ExecSQL;
// only executing insert after the last column was filled
FSM := 0; // next mode of tick-tock
Continue;
end;
/// would only come here if we made some mistake above
/// and FSM got impossible value - so no "Continue" was executed
/// show error and exit
raise EInvalidOperation.CreateFmt('FSM = %d', [FSM]);
end;
q.Transaction.CommitWork;
End;
Procedure Method3_SimplifiedFSM( const sa: array of string );
// this method is actually are streamlined method #2
// it can be made because all our steps are TOTALLY identical
// ( sans optional insert execution )
var i, FSM: integer;
Begin
FSM := 0;
for i := Low(sa) to High(sa) do begin
q.Params[ FSM ].AsWideString := sa[i];
Inc( FSM );
if FSM >= q.Params.Count then begin
q.ExecSQL; // only after (if ) last of all columns filled!
FSM := 0;
end;
end;
q.Transaction.CommitWork;
End;
Now you can debug calls like Method1(sa_OK) or Method2(sa_err1) and see how it is working and how it deals with errors

Returning a dynamic array from a function

I'm working with Delphi 7.
Here's my base class. The function load_for_edit() is supposed to return an array of strings which sounds like it does. The problem is not particularly here but further.
...
type
TStringArray = array of string;
...
function load_for_edit(): TStringArray;
...
numberOfFields: integer;
...
function TBaseForm.load_for_edit(): TStringArray;
var
query: TADOQuery;
i: integer;
begin
query := TADOQuery.Create(self);
query.Connection := DataModule1.ADOConnection;
query.SQL.Add('CALL get_' + self.table_name + '_id(' + self.id + ')');
query.Open;
numberOfFields := query.Fields.Count;
SetLength(result, query.Fields.Count);
for i := 0 to query.Fields.Count - 1 do
result[i] := query.Fields[i].Value.AsString;
end;
Next is the class that is a descendant of the base class and it's trying to receive an array from the base class's load_for_edit() function.
...
type
TStringArray = array of string;
...
procedure TPublisherEditForm.FormShow(Sender: TObject);
var
rec: TStringArray;
begin
inherited;
SetLength(rec, self.numberOfFields);
rec := load_for_edit(); // Compilation stops here
end;
But, the application won't compile. Delphi spits out this error message:
Incompatible types
So, what this means is that the function load_for_edit() returns a data type that is different from the variable rec's data type, though, as can be seen from their respective type declaration sections, their declarations are absolutely identical. I don't know what's going on here and what to do. Please, help me come up with a solution.
You've got two separate declarations of TStringArray (one in each unit), and they are not the same. (The fact that they're in two separate units makes them different. UnitA.TStringArray is not the same as UnitB.TStringArray, even if they're both declared as type TStringArray = array of string;.)
You need to use a single type declaration:
unit
BaseFormUnit;
interface
uses
...
type
TStringArray: array of string;
TBaseForm = class(...)
numberOfFields: Integer;
function load_for_edit: TStringArray;
end;
implementation
// Not sure here. It looks like you should use a try..finally to
// free the query after loading, but if you need to use it later
// to save the edits you should create it in the TBaseForm constructor
// and free it in the TBaseForm destructor instead, which means it
// should also be a field of the class declaration above.
function TBaseForm.load_for_edit(): TStringArray;
var
query: TADOQuery;
i: integer;
begin
query := TADOQuery.Create(self);
query.Connection := DataModule1.ADOConnection;
query.SQL.Add('CALL get_' + self.table_name + '_id(' + self.id + ')');
query.Open;
numberOfFields := query.Fields.Count;
SetLength(result, numberOfFields);
for i := 0 to numberOfFields - 1 do
Result[i] := query.Fields[i].Value.AsString;
end;
...
end.
Now your descendant class can access it:
unit
PublisherEditFormUnit;
interface
uses
... // Usual Forms, Windows, etc.
BaseFormUnit;
type
TPublisherEditForm = class(TBaseForm)
...
procedure FormShow(Sender: TObject);
end;
implementation
procedure TPublisherEditForm.FormShow(Sender: TObject);
var
rec: TStringArray;
begin
// No need to call SetLength - the base class does.
rec := load_for_edit(); // Compilation stops here
end;
...
end.

Generic function to increase array of pointers

I'm working in a code that has a data structure like this:
type
TData1 = record
IntField: Integer;
StrField: string;
end;
TData2 = record
DateField: TDateTime;
StrField: string;
end;
var
AData1 = array of ^TData1;
AData2 = array of ^TData2;
Sometimes I have to add an element to one of that arrays, like this:
L := Length(AData1);
SetLength(AData1, L + 1);
New(AData1[L]);
How can I write a procedure that does that job of increasing the array size and allocating memory for a new item that works for any kind of pointer? And it must be done without changing the definitions of the records (TData1, TData2) and the arrays (AData1, AData2), so it won't break existing code.
Ps: I don't have much background with pointers and that kind of programming, I'd certainly use objects and dynamic linked lists, but in this case it's a legacy code and I can't change it, at least for now.
Update:
This is not a full answer. Using TDynArray as mentioned by David may solve your problem.
Using RTTI I made a another solution for you. It is a generic solution and you can easily add more functions/capabilities. It will preserve your type declarations. Example of adding/removing records is included.
The record TDynPtArray handles any dynamic array of pointers to records. It is initialized with an Init call:
DPA.Init(TypeInfo(TData1), AData1);
Data1 := DPA.Add; // Adds a record with default values and
// returns a pointer to the record
DPA.Remove; // Finalizes/deallocates the last record and
// shrinks the dynamic array
-
uses
Windows,System.SysUtils,System.TypInfo;
Type
TPtArray = array of Pointer;
PPtArray = ^TPtArray;
TDynPtArray = record
private
FDynArray: PPtArray;
FTypeInfo: PTypeInfo;
FTypeData: PTypeData;
public
constructor Init( T: Pointer; var dynArray);
function Add : Pointer;
procedure Remove;
procedure Clear;
end;
constructor TDynPtArray.Init(T: Pointer; var dynArray);
begin
FTypeInfo := T;
if (FTypeInfo^.Kind <> tkRecord) then
raise Exception.CreateFmt('%s is not a record',[FTypeInfo^.Name]);
FTypeData := GetTypeData( FTypeInfo);
FDynArray := #dynArray;
end;
function TDynPtArray.Add: Pointer;
var
L: integer;
begin
L := Length(FDynArray^);
SetLength(FDynArray^,L+1);
GetMem( FDynArray^[L], FTypeData^.elSize);
ZeroMemory( FDynArray^[L], FTypeData^.elSize);
Result := FDynArray^[L];
end;
procedure RecordClear(var Dest; TypeInfo: pointer);
asm
{$ifdef CPUX64}
.NOFRAME
{$endif}
jmp System.#FinalizeRecord
end;
procedure TDynPtArray.Remove;
var
L: integer;
begin
L := Length(FDynArray^);
if (L = 0) then
exit;
RecordClear( FDynArray^[L-1]^,FTypeInfo); // Finalize record
FreeMem( FDynArray^[L-1], FTypeData^.elSize);
SetLength(FDynArray^,L-1);
end;
procedure TDynPtArray.Clear;
begin
while (Length(FDynArray^) <> 0) do
Self.Remove;
end;
And a little test:
type
PData1 = ^TData1;
TData1 = record
IntField: Integer;
StrField: string;
end;
TData1Arr = array of PData1;
var
AData1: TData1Arr;
Data1: PData1;
DPA: TDynPtArray;
begin
DPA.Init(TypeInfo(TData1), AData1);
Data1:= DPA.Add;
Data1^.StrField := '111';
WriteLn(Data1^.IntField);
WriteLn(Data1^.StrField);
DPA.Clear;
ReadLn;
end.

Resources