Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an associative array in Pascal using arrays for the values

I've this file:

Bulgaria = Bulgarian
Croatia = Croatian
Austria = Croatian
Czech Republic = Czech
Slovakia = Czech
Denmark = Danish
Germany = Danish
Belgium = Dutch
Netherlands = Dutch
Ireland = English
Malta = English
United Kingdom = English
Estonia = Estonian
Finland = Finnish
Belgium = French
France = French
Italy = French
Luxembourg = French
Austria = German
Belgium = German
Denmark = German
Germany = German
Italy = German
Luxembourg = German
Cyprus = Greek
Greece = Greek
Austria = Hungarian
Hungary = Hungarian
Romania = Hungarian
Slovakia = Hungarian
Slovenia = Hungarian
Ireland = Irish
United Kingdom = Irish
Croatia = Italian
Italy = Italian
Slovenia = Italian
Latvia = Latvian
Lithuania = Lithuanian
Malta = Maltese
Poland = Polish
Portugal = Portuguese
Romania = Romanian
Slovakia = Slovak
Czech Republic = Slovak
Hungary = Slovak
Slovenia = Slovenian
Austria = Slovenian
Hungary = Slovenian
Italy = Slovenian
Spain = Spanish
Sweden = Swedish
Finland = Swedish

In python, I use this code to create an associative array with a string as key and an array as value:

from collections import defaultdict
mydata = defaultdict(list)
myfile = open("myfile", "r")
for line in myfile:
    country, language = line.rstrip('\n').split(" = ")
    mydata[country].append(language)

It creates a data structure like this:

'Bulgaria' = ['Bulgarian']
'Croatia' = ['Croatian']
'Austria' = ['Croatian', 'German', 'Hungarian', 'Slovenian']
# and so on

Perl and Ruby has similar associative arrays. Go can create it with maps and append().

I see a lot of references to associative arrays in Pascal but I can't find examples of use with arrays as values.

I'm using FreePascal and I would like avoid external libraries. Can you show me an example for this?.

PS: I know this looks like homework but it's not.

like image 307
Mariano R. Avatar asked May 12 '17 20:05

Mariano R.


2 Answers

Arrays

Pascal (Delphi) language doesn't have associative arrays in the language. There are just arrays with an index of an ordinal type (i.e. integer, not string or a floating-point number). There are multi-dimensional arrays and, recently, dynamic arrays, but the index is ordinal anyway.

However, standard library units (Classes.pas and System.Generics.Collections.pas) provide classes that implement the functionality you are speaking about.

You can use new Delphi Generics, particularly TDictionary, for truly associative arrays, or the good old TStringList, just for plain string lists. The TStringList class have been extended with the Name+Delimiter+Value functionality in newer versions of Delphi. For the pairs, the TStringList is slower than TDictionary of Delphi Generics, because it just stores plain strings inside and parses them on-the-fly. The Generics, however, use efficient structures that add and remove items quickly, use hashes of the values, and thus are very fast. In the TStringList, to the contrary, random insertions and deletions are slow - O(N), but getting a string by index is momentary - O(1).

Generics

uses
  System.Generics.Collections,

procedure TestGenerics;
type
  TKey = string;
  TValue = string;
  TKeyValuePair = TPair<TKey, TValue>;
  TStringDictionary  = TDictionary<TKey, TValue>;

var
  D: TStringDictionary;
  K: TKey;
  V: TValue;
  P: TKeyValuePair;
  ContainsKey,  ContainsValue: Boolean;
begin
  D := TStringDictionary.Create;
  D.Add('Bulgaria', 'Bulgarian');
  D.Add('Croatia', 'Croatian Italian');
  K := D.Items['Bulgaria'];
  P := D.ExtractPair('Bulgaria');
  ContainsKey := D.ContainsKey('Bultaria');
  ContainsValue := D.ContainsValue('Bultarian');
  // you do not need to free P, since it is just a record in the stack
  D.Free;
end;

String List

Please note that the authors of Delphi call it Name+Value pairs, not Key+Value pairs, because, in the TStringList, these are not actually "Keys" in the means of fast access, it is just parts of the same string. They don't have special sorted indexing capabilities - just regular sorted TStringList if you wish.

Also be aware that when the TStringList object includes strings that are name-value pairs or just names, read Keys to access the name part of a string. If the string is not a name-value pair, Keys returns full string. Assigning Keys will write new name for name-value pair. This is in contrast to Names property.

Also, take into consideration, that TStringList uses continuous memory space to hold pointers to the string data, so when you add new strings, it uses a pre-allocated space for a few more entries, but then allocates a new larger memory block and copies old pointers to the new one, releasing an old block. So, if you know in advance the number of items, it's better to tell that number to TStringList to it would pre-allocate the buffer once and for all. That would not prevent from enlarging the buffer later, should you need more items then.

uses
  Classes;

procedure TestStringList;
var
  SL: TStringList;
  FullString, Separator, FName, FValue: string;
begin
  SL := TStringList.Create;
  SL.AddPair('Bulgaria', 'Bulgarian'); // add a Name -> Value pair
  SL.AddPair('Croatia', 'Croatian Italian');

  // Names and KeyNames is the same
  FName := SL.Names[0]; // Indicates the name part of strings that are name-value pairs.
  FName := SL.KeyNames[0];
  FValue := SL.Values['Bulgaria']; // Represents the value part of a string associated with a given name, on strings that are name-value pairs.
  FValue := SL.ValueFromIndex[0]; // Represents the value part of a string with a given index, on strings that are name-value pairs.

  FullString := SL.Strings[0]; // References the strings in the list by their positions (the whole Name+Separator+Value pair)
  Separator := SL.NameValueSeparator; // Indicates the character used to separate names from values.

  SL.Free;
end;
like image 156
Maxim Masiutin Avatar answered Sep 18 '22 11:09

Maxim Masiutin


A demo program follows.

Open Lazarus, create an Application, and add the unit fgl to the uses clause of the unit for the form:

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, fgl;

Add a TButton (Button1) and a TMemo (Memo1) to the form and give the button the following code:

{ Can't find anything suitable in the Lazarus runtime. I'm sure this }
{ can be improved.                                                   }
function Strip(const S: string): string;
var
  left, right: Integer;
begin
  if S = '' then
  begin
    Result := S;
    Exit;
  end;
  left := 1;
  while S[left] in [' ', #9] do
    Inc(left);
  right := Length(S);
  while (right > 0) and (S[right] in [' ', #9]) do
    Dec(right);
  Result := Copy(S, left, right - left + 1);
end;

type
  TMap = specialize TFPGMap<string, TStringList>;

procedure TAssocForm.Button1Click(Sender: TObject);
var
  mydata: TMap;
  myfile: Text;
  line: string;
  country: string;
  language: string;
  mypos: Integer;
  SL: TStringList;
  I: Integer;
begin
  { Error handling needs to be added, e.g. if file doesn't exist, or if 
    a line doesn't contain an =, etc. etc. }
  mydata := TMap.Create;

  { Open file 'myfile.txt' for reading. }
  System.Assign(myfile, '/Users/xxx/yyy/myfile.txt'); { adjust accordingly }
  Reset(myfile);

  { Read lines. }
  while not Eof(myfile) do
  begin
    Readln(myfile, line);
    mypos := Pos('=', line);

    { Split line into country and language. }
    country := Strip(Copy(line, 1, mypos - 1));
    language := Strip(Copy(line, mypos + 1, MaxInt));

    { If key not present yet, add a new string list. }
    if mydata.IndexOf(country) < 0 then
      mydata.Add(country, TStringList.Create);

    { add language to string list of country. }
    mydata[country].Add(language);
  end;
  System.Close(myfile);

  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;
  for I := 0 to mydata.Count - 1 do
  begin
    { Get key. }
    country := mydata.Keys[I];
    line := country + ' -> ';

    { Get string list. }
    SL := mydata[country];

    { Get languages in the string list. }
    for language in SL do
      line := line + language + ' ';

    { Add line to memo. }
    Memo1.Lines.Add(Strip(line));
  end;
  Memo1.Lines.EndUpdate;

  { Free the string lists. }
  for I := 0 to mydata.Count - 1 do
    mydata[mydata.Keys[I]].Free;
end;

end.

Run the program and click the button. The memo will be filled with the countries and the languages that are spoken there, e.g.

Bulgaria -> Bulgarian 
Croatia -> Croatian Italian 
Austria -> Croatian German Hungarian Slovenian 
Czech Republic -> Czech Slovak 
Slovakia -> Czech Hungarian Slovak 
etc...
like image 25
Rudy Velthuis Avatar answered Sep 20 '22 11:09

Rudy Velthuis