For a simulation program I'm working in Delphi 2010. The simulation isn't a problem but I need to use large collection of data which gives a problem. The data is available in excel sheets, so there is no need to edit this data in Delphi, but collecting this data from the excel sheets takes around 10min. This isn't a problem as long as you don't need to collect the data every time the program runs. So I made a program which collects all the data makes it visible, not problems here,and then store it. However I can't store it to a "Delphi format" , without losing the structure, so it can be loaded in a few seconds.
I'm not that experienced in Delphi and I searched a long time for the solution but couldn't understand what was best. I think my way of structuring the data is wrong but it was simple and worked. However if there are better ways of storing the data please say so, but remember that I need some more explanation than just use 'a xml file', 'generict, or 'Ttreeview'. (have read it but wasn't able to use it).
The data is for: I made this product, The next product I make is this, so do I need to clean? True or false.
The data is stores as a class(TObject) with Productnumber (integer) and a List which contains all products that could be made next.This list contains another class(TObject) with an Productnumber (integer) and a do I need to clean(boolean). I want to save this structure in a file, without losing the data and read it back to the same structure.
I hope someone could help. Thank you in advance.
Update: The code to provide a little more information (modified to English)
Clean_from = class(TObject)
public
myfromNumber : Integer;
mylist : TList;
published
constructor Create;
End
Clean_To = class(TObject)
public
myToNumber : Integer;
Clean : Boolean;
End;
constructor Clean_from.Create;
begin
inherited Create;
myList := Tlist.Create;
end;
For i = 0 to 100 do
begin
From:= Clean_from.create;
for j := 0 to 10 do
begin
To := Clean_To.create;
To.clean := true or false;
From.myList.add(To);
end;
GlobalList.add(from);
end;
And now I want to save the global list with all the content so I could load it with the same structure.
What you need is the so-called "serialization" mechanism.
1. The standard way
1.1 SaveToStream
In Delphi, we usually implement a SaveToStream method, which will save the content of each object in a destination TStream (either a TFileStream or a TMemoryStream).
You'll have to write the serialization by hand.
1.2 DFM-like streaming
See TWriter / TReader classes.
If you define your data in published properties, you are able to serialize them using those standard Delphi classes.
For some methods able to serialize any TCollection to and from JSON content, see this blog article.
2. The RTTI
See for instance this SO question.
In particular, the new enhanced RTTI (available since Delphi 2010) opens new opportunities to serialization.
3. Use records instead of classes
If each item does not store a lot of content (some integer/boolean), it may make sense to use records instead of objects. For speed and memory consumption/fragmentation, it may be worth it.
Here is some wrapper able to serialize any dynamic array, even containing nested records or dynamic arrays.
4. Use a database engine
Perhaps the better approach is not to have your data stuck in a non-evolving binary form, proprietary to your application. If you want to add a property, you'll have to manage it by hand. Or if you want to access your data from other applications, it may be difficult.
There are a lot of database solutions around - instead of using an external database (like MS SQL, FireBird or Oracle), it could be a good idea to embed the database inside your application (much easier to install). Worth mentioning SQLite which has a lot of wrappers, including our version (which will allow you to change to any other database if you want to use MS SQL or Oracle instead).
You have other solutions around - see this SO question - and if you need performance, take a look at our Big Table library.
Add SaveToStream() and LoadFromStream() methods to your data object which, well, save the data to a stream and load data from a stream.
type
TMyData = class(TObject)
private
FChildProducts: TList;
FProductnumber : integer;
FClean: boolean;
public
procedure LoadFromStream(const aStream: TStream);
procedure SaveToStream(const aStream: TStream);
published
property Productnumber: Integer read FProductnumber write FProductnumber;
property Clean: Boolean reas FClean write FClean;
end;
procedure TMyData.LoadFromStream(const aStream: TStream);
var x, cnt: Integer;
cD: TMyData;
begin
aStream.Read(FProductnumber, SizeOf(FProductnumber));
aStream.Read(FClean, SizeOf(FClean));
// read number of child products
aStream.Read(cnt, SizeOf(cnt));
// load child objects
for x := 1 to cnt do begin
cD := TMyData.create;
cD.LoadFromStream(aStream);
FChildProducts.Add(cD);
end;
end;
procedure TMyData.SaveToStream(const aStream: TStream);
var x: Integer;
begin
aStream.Write(FProductnumber, SizeOf(FProductnumber));
aStream.Write(FClean, SizeOf(FClean));
// save number of child products
x := FChildProducts.Count;
aStream.Write(x, SizeOf(x));
// save child objects
for x := 0 to FChildProducts.Count - 1 do
(FChildProducts[x] as TMyData).SaveToStream(aStream);
end;
I assume you have some list of "root objects" so you can make an function or method which saves/loads them to/from stream ie
function SaveDataList(const List: TList;const aFileName: string);
var x: Integer;
FS: TFileStream;
begin
FS := TFileStream.Create(aFileName, ...);
try
// save file version
x := 1;
FS.Write(x, SizeOf(x));
// save number of products
x := List.Count;
FS.Write(x, SizeOf(x));
// save objects
for x := 0 to List.Count - 1 do
(List[x] as TMyData).SaveToStream(FS);
finally
FS.Free;
end;
end;
This is the general idea... how to load data back should be clear too. The file version thing is there so that when the data object changes (ie you add some property) you can increment the version number so that in the loading code you can load data into right version of the data object.
Related
Overview
This question is a second attempt based on this one I recently asked: How can I make a TList property from my custom control streamable?
Although I accepted the answer in that question and it worked, I soon realized that TCollection is not the solution or requirement I was looking for.
Requirements
To keep my requirements as simple and clear to understand as possible, this is what I am attempting to:
Derive a new custom control based on TCustomListBox
Replace the Items property with my own Items type, eg a TList.
The TList (Items property) will hold objects, each containing a caption and a image index property etc.
Ownerdraw my listbox and draw its icons and text etc.
Create a property editor to edit the Items at design-time.
With that in mind, I know how to create the custom control, I know how to work with TList or even TObjectList for example, I know how to ownerdraw the control and I also know how to create the property editor.
Problem
What I don't know is how to replace the standard listbox Items type with my own? well I kind of do (publishing my own property that shares the same name), only I need to make sure it is fully streamable with the dfm.
I have searched extensively on this subject and have tried studying code where TListView and TTreeView etc publishes its Items type but I have found myself more confused than ever.
In fact I came across this very old question asked by someone else on a different website which asks very much what I want to do: Streaming a TList property of a component to a dfm. I have quoted it below in the event the link is lost:
I recently wrote a component that publishes a TList property. I then created a property editor for the TList to enable design-time editing. The problem is that the TList doesn't stream to the dfm file, so all changes are lost when the project is closed. I assume this is because TList inherits from TObject and not from TPersistant. I was hoping there was an easy work around for this situation (or that I have misunderstood the problem to begin with). Right now all I can come up with is to switch to a TCollection or override the DefineProperties method. Is there any other way to get the information in the TList streamed to and from the dfm?
I came across that whilst searching keywords such as DefineProperties() given that this was an alternative option Remy Lebeau briefly touched upon in the previous question linked at the top, it also seemed to be the answer to that question.
Question
I need to know how to replace the Items (TStrings) property of a TCustomListBox derived control with my own Items (TList) or Items (TObjectList) etc type but make it fully streamable with the dfm. I know from previous comments TList is not streamable but I cannot use TStrings like the standard TListBox control does, I need to use my own object based list that is streamable.
I don't want to use TCollection, DefineProperties sounds promising but I don't know how exactly I would implement this?
I would greatly appreciate some help with this please.
Thank you.
Override DefineProperties procedure in your TCustomListBox (let's name it TMyListBox here). In there it's possible to "register" as many fields as you wish, they will be stored in dfm in the same way as other fields, but you won't see them in object inspector. To be honest, I've never encountered having more then one property defined this way, called 'data' or 'strings'.
You can define 'normal' property or binary one. 'Normal' properties are quite handy for strings, integers, enumerations and so on. Here is how items with caption and ImageIndex can be implemented:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure ReadData(reader: TReader);
procedure WriteData(writer: TWriter);
protected
procedure DefineProperties(filer: TFiler); override;
//other stuff
public
//other stuff
property Items: TList read fItems; //not used for streaming, not shown in object inspector. Strictly for use in code itself. We can make it read-only to avoid memory leak.
published
//some properties
end;
that's DefineProperties implementation:
procedure TMyListBox.DefineProperties(filer: TFiler);
begin
filer.DefineProperty('data', ReadData, WriteData, items.Count>0);
end;
fourth argument, hasData is Boolean. When your component is saved to dfm, DefineProperties is called and it's possible to decide at that moment is there any data worth saving. If not, 'data' property is omitted. In this example, we won't have this property if there is no items present.
If we expect to ever use visual inheritance of this control (for example, create a frame with this listBox with predefined values and then eventually change them when put to form), there is a possibility to check, is value of this property any different than on our ancestor. Filer.Ancestor property is used for it. You can watch how it's done in TStrings:
procedure TStrings.DefineProperties(Filer: TFiler);
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then
begin
Result := True;
if Filer.Ancestor is TStrings then
Result := not Equals(TStrings(Filer.Ancestor))
end
else Result := Count > 0;
end;
begin
Filer.DefineProperty('Strings', ReadData, WriteData, DoWrite);
end;
This would save a little bit of space (or lots of space if image is stored within) and sure is elegant, but in first implementation it can well be omitted.
Now the code for WriteData and ReadData. Writing is much easier usually and we may begin with it:
procedure TMyListBox.WriteData(writer: TWriter);
var i: Integer;
begin
writer.WriteListBegin; //in text dfm it will be '(' and new line
for i:=0 to items.Count-1 do begin
writer.WriteString(TListBoxItem(items[I]).caption);
writer.WriteInteger(TListBoxItem(items[I]).ImageIndex);
end;
writer.WriteListEnd;
end;
In dfm it will look like this:
object MyListBox1: TMyListBox
data = (
'item1'
-1
'item2'
-1
'item3'
0
'item4'
1)
end
Output from TCollection seems more elegant to me (triangular brackets and then items, one after another), but what we have here would suffice.
Now reading it:
procedure TMyListBox.ReadData(reader: TReader);
var item: TListBoxItem;
begin
reader.ReadListBegin;
while not reader.EndOfList do begin
item:=TListBoxItem.Create;
item.Caption:=reader.ReadString;
item.ImageIndex:=reader.ReadInteger;
items.Add(item); //maybe some other registering needed
end;
reader.ReadListEnd;
end;
That's it. In such a way rather complex structures can be streamed with ease, for example, two-dimensional arrays, we WriteListBegin when writing new row and then when writing new element.
Beware of WriteStr / ReadStr - these are some archaic procedures which exist for backward compatibility, ALWAYS use WriteString / ReadString instead!
Other way to do is to define binary property. That's used mostly for saving images into dfm. Let's say, for example, that listBox has hundreds of items and we'd like to compress data in it to reduce size of executable. Then:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure LoadFromStream(stream: TStream);
procedure SaveToStream(stream: TStream);
protected
procedure DefineProperties(filer: TFiler); override;
//etc
end;
procedure TMyListBox.DefineProperties(filer: TFiler);
filer.DefineBinaryProperty('data',LoadFromStream,SaveToStream,items.Count>0);
end;
procedure TMyListBox.SaveToStream(stream: TStream);
var gz: TCompressionStream;
i: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TCompressionStream.Create(stream);
try
value:=items.Count;
//write number of items at first
gz.Write(value, SizeOf(value));
//properties can't be passed here, only variables
for i:=0 to items.Count-1 do begin
item:=TListBoxItem(items[I]);
value:=Length(item.Caption);
//almost as in good ol' Pascal: length of string and then string itself
gz.Write(value,SizeOf(value));
gz.Write(item.Caption[1], SizeOf(Char)*value); //will work in old Delphi and new (Unicode) ones
value:=item.ImageIndex;
gz.Write(value,SizeOf(value));
end;
finally
gz.free;
end;
end;
procedure TMyListBox.LoadFromStream(stream: TStream);
var gz: TDecompressionStream;
i: Integer;
count: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TDecompressionStream.Create(stream);
try
gz.Read(count,SizeOf(count)); //number of items
for i:=0 to count-1 do begin
item:=TListBoxItem.Create;
gz.Read(value, SizeOf(value)); //length of string
SetLength(item.caption,value);
gz.Read(item.caption[1],SizeOf(char)*value); //we got our string
gz.Read(value, SizeOf(value)); //imageIndex
item.ImageIndex:=value;
items.Add(item); //some other initialization may be needed
end;
finally
gz.free;
end;
end;
In dfm it would look like this:
object MyListBox1: TMyListBox1
data = {
789C636260606005E24C86128654865C064386FF40802C62C40002009C5607CA}
end
78 is sort of signature of ZLib, 9C means default compression, so it works (there are only 2 items actually, not hundreds). Of course, this is just one example, with BinaryProperties any possible format may be used, for example saving to JSON and putting it into stream, or XML or something custom. But I'd not recommend to use binary unless it's absolutely inevitable, because it's difficult to see from dfm, what happens in component.
It seems like good idea to me to actively use streaming when implementing component: we can have no designer at all and set all values by manually editing dfm and see if component behaves correctly. Reading/loading itself can be tested easily: if component is loaded, then saved and text is just the same, it's all right. It's so 'transparent' when streaming format is 'human-readable', self-explaining that it often overweighs drawbacks (like file size) if there are any.
I am developing an application and I have to upload data from CSV files into a DB tables. Problem is, I don’t have CSV files but I have flat text files to be converted into CSV.
An additional problem is, as the application is used by several customers who have different systems, I have different flat text files with different layouts.
What I want to achieve is to create an application that loads “rules” from a special file; these rules will be processed with the flat text file in order generate the CSV file. The application that converts from flat file to CSV would be the same, just the set of rules would be different.
How can I achieve this? What is the best practice you recommend?
It depends on the complexity of the rules. If the only varying input is the names of the columns and the separator used, then it's pretty easy, but if you want to be able to parse completely different formats (like XML or so) as well, then it's a different story.
I myself would choose to implement a base class for a 'record' reader that reads records from a file and outputs them to a dataset or CSV.
Then, you can implement child classes that implement reading different source formats.
If you like, you can then add specific rules for those format, so you can make a generic XMLReader that descends from BaseReader, but which allows for configurable column names. But I would start with a bunch of hard-coded readers for the formats you got, until it's more clear which dialects of those formats you may encounter.
Edit: On request, an example of how it could look like.
Note, this example is far from ideal! It reads a custom format, transfers it to one specific table structure and saves that as an CSV file. You may want to split it a little further, so you can reuse the code for different table structures. Especially the field defs, you may want to be able to set in a descendant class or maybe a factory class.
But for the sake of simplicity I have taken a more rigid approach and put a little too much intelligence in one single base class.
The base class has the logic needed to create an in-memory dataset (I used a TClientDataSet). It can 'Migrate' a file. In practice, this means it reads, validates and exports the file.
The reading is abstract and must be implemented in a child class. It should read the data to the in memory dataset. That allows you to do all necessary validation in the client dataset. This allows to you enforce field types and sized and do any additional checking if you need to, in a database/file format agnostic way.
The validating and writing is done using the data in the dataset. From the moment where the source file is parsed to a dataset, no knowledge about the source file format is required anymore.
Declaration:
Don't forget to use DB, DBClient.
type
TBaseMigrator = class
private
FData: TClientDataset;
protected
function CSVEscape(Str: string): string;
procedure ReadFile(AFileName: string); virtual; abstract;
procedure ValidateData;
procedure SaveData(AFileName: string);
public
constructor Create; virtual;
destructor Destroy; override;
procedure MigrateFile(ASourceFileName, ADestFileName: string); virtual;
end;
Implementation:
{ TBaseReader }
constructor TBaseMigrator.Create;
begin
inherited Create;
FData := TClientDataSet.Create(nil);
FData.FieldDefs.Add('ID', ftString, 20, True);
FData.FieldDefs.Add('Name', ftString, 60, True);
FData.FieldDefs.Add('Phone', ftString, 15, False);
// Etc
end;
function TBaseMigrator.CSVEscape(Str: string): string;
begin
// Escape the string to a CSV-safe format;
// Todo: Check if this is sufficient!
Result := '"' + StringReplace(Result, '"', '""', [rfReplaceAll]) + '"';
end;
destructor TBaseMigrator.Destroy;
begin
FData.Free;
inherited;
end;
procedure TBaseMigrator.MigrateFile(ASourceFileName, ADestFileName: string);
begin
// Read the file. Descendant classes need to override this method.
ReadFile(ASourceFileName);
// Validation. Implemented in base class.
ValidateData;
// Saving/exporting. For now implemented in base class.
SaveData(ADestFileName);
end;
procedure TBaseMigrator.SaveData(AFileName: string);
var
Output: TFileStream;
Writer: TStreamWriter;
FieldIndex: Integer;
begin
Output := TFileStream.Create(AFileName,fmCreate);
Writer := TStreamWriter.Create(Output);
try
// Write the CSV headers based on the fields in the dataset
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Column headers are escaped, but this may not be needed, since
// they likely don't contain quotes, commas or line breaks.
Writer.Write(CSVEscape(FData.Fields[FieldIndex].FieldName));
end;
Writer.WriteLine;
// Write each row
FData.First;
while not FData.Eof do
begin
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Escape each value
Writer.Write(CSVEscape(FData.Fields[FieldIndex].AsString));
end;
Writer.WriteLine;
FData.Next
end;
finally
Writer.Free;
Output.Free;
end;
end;
procedure TBaseMigrator.ValidateData;
begin
FData.First;
while not FData.Eof do
begin
// Validate the current row of FData
FData.Next
end;
end;
An example child class: TIniFileReader, which reads inifile sections as if they were database records. As you can see, you only need to implement the logic to read the file.
type
TIniFileReader = class(TBaseMigrator)
public
procedure ReadFile(AFileName: string); override;
end;
{ TIniFileReader }
procedure TIniFileReader.ReadFile(AFileName: string);
var
Source: TMemIniFile;
IDs: TStringList;
ID: string;
i: Integer;
begin
// Initialize an in-memory dataset.
FData.Close; // Be able to migrate multiple files with one instance.
FData.CreateDataSet;
// Parsing a weird custom format, where each section in an inifile is a
// row. Section name is the key, section contains the other fields.
Source := TMemIniFile.Create(AFileName);
IDs := TStringList.Create;
try
Source.ReadSections(IDs);
for i := 0 to IDs.Count - 1 do
begin
// The section name is the key/ID.
ID := IDs[i];
// Append a row.
FData.Append;
// Read the values.
FData['ID'] := ID;
FData['Name'] := Source.ReadString(ID, 'Name', '');
// Names don't need to match. The field 'telephone' in this propriety
// format maps to 'phone' in your CSV output.
// Later, you can make this customizable (configurable) if you need to,
// but it's unlikely that you encounter two different inifile-based
// formats, so it's a waste to implement that until you need it.
FData['Phone'] := Source.ReadString(ID, 'Telephone', '');
FData.Post;
end;
finally
IDs.Free;
Source.Free;
end;
end;
This is very similar to the problems faced by "screen scrapers". If end users are intended to be able to use this, I would avoid regular expressions (except as an internal implementation detail, if needed) and not expose raw regular expression editing to end users.
Instead, I would let them load up samples of their data files, and construct their rules visually, with a drag, and drop style.
Click a "Match text" button, click and drag to select a rectangular region on the screen. Have options so that it might be allowed to move a certain amount up or down or left or right, if the format isn't precise or repeatable. Establish limits on how far you can go outside the original box.
Click a "grab text" button, click and drag to a rectangular or non-rectangular (flow) area on the screen. Name the output with a field, and give it a type (integer, string[x], etc). Similar limits apply as step 1.
Click save and the template rules are written to disk. Load a different file and see if the rules still apply nicely.
Relevant wikipedia topic.
Delphi - NOVICE programmer - (meaning don't expect advanced concepts to be already known )
I am working on an application, part of which is a scheduling function. I look at a group of appointments. These appointments fall on a specific date. I need to create a consolidated view of all the appointments on each date. By this, I mean that I create a "grouping" of data about Jan 17th, this is going to be how many appointments are on that date, when they start, when they stop, etc. This may be an array, a Record, a class, don't know at this point. I may have one for the 17th, then the next one is for the 22nd, then the next is for the 24, and then I may have one every day for the next 35 days... I will have either 0 or 1 structures/containers per day, and I can see having 3 to 5 months months of these...Which ones exist and which ones don't exist will be very fluid. This structure is a consolidated view, meaning that I get some of the information from one appointment, and then some of the information from the next appointment, so as I am reading each appointment, I need to be able to find this structure quickly.
I need these structure to be memory based, so they are fast. They MAY be of different sizes (depending on the number of appointments on that day.
REQUIREMENTS
So, I need to be able to create these structures on the fly. (for example, I have just read an appointment for Jan 22, and need to update the structure, but it doesn't exists yet, so I need to make it on the fly).
I need to be able to find them them quickly. (Hash on TDate maybe).
Each one of the structures will hold multiple data types (boolean, TDateTime, TStringlist, etc).
I have Delphi 2010 if that helps...
What structure am I looking for? Easy, fast, already included with D2010 (or free) are all important.
Thanks
GS
UPDATED INFO:
So it appears the TDictionary or TObjectDictionary is the way to go...
I have decided (I think...) to use a record to hold my base information, and then store those records in either TDictionary or TObjectDictionary. I am running into a challenge in both of them. In TDictionary, I cannot figure out how to free the records when I am done with them (since they are pointers), and with TObjectDictionary, I cannot create it with a record type... Any help appreciated. Code samples are...
// Create the base record definition that will be put in the TObjectDictionary/TDictionary
type
TSummaryAppt = record
Date : TDate;
SA_ID : Integer;
BusyFlag : Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
end;
If I go TObjectDictionary, this will not work...
var
Dic : TObjectDictionary<Integer,TSummaryAppt>;
begin
Dic := TObjectDictionary<Integer,TSummaryAppt>.Create([doOwnsKeys, doOwnsValues]);
The Create line fails on execution (compiles fine) with Invalid Class Typecast.
TDictionary appears to be a little friendlier, but I have to deallocate my memory....
var
Dic : TDictionary<Integer,TSummaryAppt>;
rec : ^TSummaryAppt;
p : TSummaryAppt;
i : Integer;
begin
Dic := TDictionary<Integer,TSummaryAppt>.Create;
// Now add some records. THese have to be created dynamically
// because I dont know at compile time how many there are.
new(rec);
rec.Date := now;
rec.SA_ID := 3;
Dic.Add(1, rec^);
new(rec);
rec^.Date := now;
rec^.SA_ID := 5;
Dic.Add(2, rec^);
new(rec);
rec^.Date := now;
rec^.SA_ID := 7;
Dic.Add(3, rec^);
// Test ...
for p in Dic.Values do begin
ShowMessage(IntToStr(p.SA_ID));
end;
// Now free everything. HERE IS WHERE I AM HAVING PROBLEMS...
// What should I be doing?
for p in Dic.values do
p.dispose;
Dic.Values.Free;
Dic.Keys.Free;
Dic.Free;
end;
Any and all help appreciated. What should I be doing different?
Thanks so much!
GS
Regarding your update to the question (which really needs to be a new question), here's what you need to do.
First of all, since both your key and value for the dictionary are value types you need to use TDictionary<K,V>. When you add items to the dictionary, a copy is made of both key and value so this means you need not do any dynamic allocation.
Your code should look like this:
type
TSummaryAppt = record
Date: TDate;
SA_ID: Integer;
BusyFlag: Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes: Integer;
end;
....
var
Dic: TDictionary<Integer, TSummaryAppt>;
rec: TSummaryAppt;
....
// create the dictionary
Dict := TDictionary<Integer, TSummaryAppt>.Create;
....
// initialise rec, in your code you would put real values in
FillChar(rec, SizeOf(rec), 0);
rec.Date := now;
rec.SA_ID := 3;
Dict.Add(1, rec);
rec.Date := now;
rec.SA_ID := 5;
Dict.Add(2, rec);
//etc.
When you have finished with the dictionary all you need to do is free it. The dictionary owns all the contents and will clean up.
Dict.Free;
You may well prefer to wrap up the functionality of the dictionary and expose it through a higher-level interface. So you may have an Add method that received as parameters all the fields of the value to be added. And you may want an update method that received just the mutable fields.
Do you know the generic TDictionary collection?
class TAppointmentCalendar = TDictionary<TDate, TAppointments>
end;
TAppointments also can be a generic class, based on Generics.Collections.TObjectList:
class TAppointments = TObjectList<TAppointment>
end;
These classes can be extended as needed, for example to add properties or data aggregation methods.
Then instantiate a Calendar
Cal := TAppointmentCalendar.Create;
Cal.Add(MyDate, AppointmentsForThisDay);
or retrieve Appointments
var
Appointments: TAppointments
begin
Cal.TryGetValue(ADate, Appointments);
...
TDictionary performs hash based Key lookups.
First of all, as a novice in data structures, it is worth buying and reading this great book: The Tomes of Delphi: Algorithms and Data Structures - By Julian Bucknall.
Here are some potential layout to implement your application.
A. NoSQL database. For instance, take a look at our BigTable Open Source components.
The root component TSynBigTableis available to records within a file-based database. It is light, and very optimized for speed.
Two of their children do handle fields within records. The field layout can change on the fly. See TSynBigTableRecord and TSynBigTableMetaData.
B. Use a regular SQL database, either like TClientDataSet or direct via SQL (you'll find some Open Source components on a static SQLite3 engine (static, i.e. with no external dll needed).
Using SQLite3 as application data, is a very good idea. It is the main purpose of this library - it is used by a lot of programs, like FireFox or Chrome, or even in most Cell phones OS.
In order to make queries fast, you'll have to create indexes on some columns (e.g. date field), and therefore query results will be immediate.
I do not recommend "to be able to create these structures on the fly". It is not a good programming practice IMHO - or it will become very complex: it would need to store the field layout within the records, so it is not a good path for a novice Delphi programmer.
You should better make your data structures open enough to handle any kind of data. With SQLite3 you can serialize your data as BLOB or text (e.g. with JSON). This is e.g. what we allow in our Open Source mORMot ORM - it is client-server, but can be used stand alone. I'd recommend taking a look at our framework documentation, especially the SAD document which tries to present some design approach, like test-driven, ORM or SOA.
C. If you want a TDictionary kind of storage, take a look at our TDynArray wrapper, which handle the same methods but has some unique features like automated serialization or multiple indexes (not handled by TDictionary), which are mandatory for your request.
AFTER QUESTION UPDATE
Some code using TDynArray:
type
TSummaryAppt = record
Date : TDate;
SA_ID : Integer;
BusyFlag : Array[1..36] of Boolean; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
end;
TSummaryApptDynArray = array of TSummaryAppt;
var rec: TSummaryAppt;
SAs: TSummaryApptDynArray;
SA: TDynArray;
F: TFileStream;
begin
SA.Init(TypeInfo(TSummaryApptDynArray),SAs);
rec.Date := now;
rec.SA_ID := 3;
SA.Add(rec); // rec is now added in SAs[]
assert(length(SAs)=1); // or SA.Count=1
assert(SAs[0].SA_ID=3);
for rec in SAs do // will work like any dynamic array
ShowMessage(IntToStr(p.SA_ID));
F := TFileStream.Create('datafile',fmCreate);
SA.SaveToStream(F); // a TDictionary won't do that
F.Free;
SA.Clear;
assert(length(SAs)=0); // or SA.Count=0
F := TFileStream.Create('datafile',fmOpenRead);
SA.LoadFromStream(F); // a TDictionary won't do that
F.Free;
assert(length(SAs)=1); // or SA.Count=1
assert(SAs[0].SA_ID=3);
for rec in SAs do // will work like any dynamic array
ShowMessage(IntToStr(p.SA_ID));
// you need nothing to free the memory, since both are handled by the compiler
end;
Of course, your array may be stored directly in one block, since it contains only plain data (double, integer, booleans); but our TDynArray wrapper is able to handle any string or other dynamic array within.
Some code using our ORM:
type
TBusyFlag = set (1..36);
TSummaryAppt = class(TSQLRecord)
private
fDate : TDate;
BusyFlag : TBusyFlag; // 15 minute periods...
PCTFree: Double;
LargestFreeMinutes : integer;
published
// already contains an ID: integer field
property Date : TDate read fDate write fDate;
property BusyFlag : TBusyFlag read fBusyFlag write fBusyFlag ; // 15 minute periods...
property PCTFree: Double read fPCTFree write fPCTFree;
property LargestFreeMinutes : integer read fLargestFreeMinutes write fLargestFreeMinutes;
end;
// then initialize the database model and use your database:
Model := TSQLModel.Create([TSummaryAppt]);
Client := TSQLRestClientDB.Create(Model,nil,'FileName',TSQLRestServerDB);
Client.Server.CreateMissingTables(0); // will create the database if needed
...
rec := TSummaryAppt.Create;
rec.Date := Now;
rec.ID := 3; // but the ORM may create one unique ID for you
Client.Add(rec);
rec.Date := 0;
Client.Retrieve(3,rec);
...
rec.Free;
Client.Free;
Model.Free;
Our ORM is here used locally, all in one executable, creating a SQLite3 database for data storage. But if you change TSQLRestClientDB into TSQLite3HttpClient and TSQLRestServerDB+TSQLite3HttpServer, you'll be able to use your data remotely, via standard JSON (and also from an AJAX application). Without modifying your client code. And if you want to store your data with something else than SQLite3 (even up to Oracle, or an in-memory database), you can.
Are there any Delphi serialization libraries that are capable of serializing records and arrays of records instead of classes?
#Max you can use the TJvAppXMLFileStorage component from JEDI to serialize an record or an array of records.
you can use the procedure called WriteBinary to store the data and ReadBinary to read.
unfortunately there is not much documentation on this component, so here you have an very simple example for store a single record (for an array of records you can easily modify this source code).
The record structure
type
MyRecord= record
Field1 : Integer;
Field2 : Double;
Field3 : String[20];
Field4 : String[20];
end;
Save an record
Procedure SaveMyRecord(Rec : MyRecord);
var
MyStore: TJvAppXMLFileStorage;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.FileName:='C:\temp\record.xml';
//this component supports store multiples objects to the same file, so you need use an identifier for you particular object, in this case i'm use the Dummy name.
MyStore.WriteBinary('Dummy', #Rec,sizeof(Rec));
MyStore.Xml.SaveToFile(MyStore.FileName);
finally
MyStore.Free;
end;
end;
this procedure create an XML file like this, the data is encoded in an hexadecimal format.
<?xml version="1.0" encoding="iso-8859-1"?>
<Configuration>
<Dummy>84030000000000003333333333331F400D737472696E6720746573742031000000000000000D737472696E672074657374203200000000000000000000000000</Dummy>
</Configuration>
Read the persisted data
Procedure LoadMyRecord(var Rec : MyRecord);
var
MyStore: TJvAppXMLFileStorage;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.FileName:='C:\temp\record.xml';//point to the same file
MyStore.Xml.LoadFromFile(MyStore.FileName); //load the file
MyStore.ReadBinary('Dummy', #Rec,sizeof(Rec));//use the Dummy identifier and pass the record as an pointer
finally
MyStore.Free;
end;
end;
Check this full project (tested in Delphi 7)
program ProjectPersistRecord;
{$APPTYPE CONSOLE}
uses
SysUtils,
JvAppXMLStorage;
type
MyRecord= record
Field1 : Integer;
Field2 : Double;
Field3 : String[20];
Field4 : String[20];
end;
Procedure SaveMyRecord(Rec : MyRecord);
var
MyStore: TJvAppXMLFileStorage;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.FileName:='C:\temp\record.xml';
MyStore.WriteBinary('Dummy', #Rec,sizeof(Rec));
MyStore.Xml.SaveToFile(MyStore.FileName);
finally
MyStore.Free;
end;
end;
Procedure LoadMyRecord(var Rec : MyRecord);
var
MyStore: TJvAppXMLFileStorage;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.FileName:='C:\temp\record.xml';
MyStore.Xml.LoadFromFile(MyStore.FileName);
MyStore.ReadBinary('Dummy', #Rec,sizeof(Rec));
finally
MyStore.Free;
end;
end;
Var
Rec : MyRecord;
begin
//Fill the record
Rec.Field1:=900;
Rec.Field2:=7.8;
Rec.Field3:='string test 1';
Rec.Field4:='string test 2';
SaveMyRecord(Rec); //save the record
FillChar(Rec,SizeOf(Rec),#0); //clear the record variable
LoadMyRecord(Rec);//restire the record data
//show the loaded data
Writeln(rec.field1);
Writeln(rec.field2);
Writeln(rec.field3);
Writeln(rec.field4);
Readln;
end.
If you have Delphi 2010, you might want to take a look at DeHL. It contains a serialization library that can handle pretty much any data type.
Another solution, working from Delphi 5 up to XE2, is available in one of our OpenSource unit.
In fact, it implements:
Some low-level RTTI functions for handling record types: RecordEquals, RecordSave, RecordSaveLength, RecordLoad;
A dedicated TDynArray object, which is a wrapper around any dynamic array, able to expose TList-like methods around any dynamic array, even containing records, strings, or other dynamic arrays. It's able to serialize any dynamic array.
Serialization uses an optimized binary format, and is able to save and load any record or dynamic array as RawByteString. You have also JSON serialization at hand, including custom layout - see sustom JSON serialization of records.
Being a record, if you don't have properties, I don't believe you're any further ahead, trying to use any of the persistence frameworks (like DeHL).
The accepted answer, while technically correct, is dubious in real-world utility, and has many long term support-nightmare scenarios if you use it in production. DON'T DO IT.
If your program is just a bit of ad-hoc'ery, may I humbly suggest that you rock it old-school with a "file of record", a classic Turbo Pascal technique that still works.
I am trying to make an AlarmSystem in Delphi 7, Windows XP. I have to register alarms in a Database (MS SQL Server 2000). But what if the server is down??? Well, I can imagine that I have to persist objects of TAlarm type. So, how can I do this? Maybe inheriting from TComponent??? Please, how can I do this??
Thanks a lot.
I am sorry about my English.
Here you have more info...
TAlarm is a class that descends from TObject, basically. There are 10 more classes that descend from TAlarm (some types of alarms). TAlarm has a field named FParams : TParams, and the child classes only have an Execute method. The field FParams can be of different types: TAlarmX1_Params, TAlarmX2_Params, etc, etc, etc.
You can inheriting from TPersistent and then you can use the TJvAppXMLFileStorage (JVCL) component to serialize the TAlarm class.
Save a Object
uses
JvAppXMLStorage;
Procedure SaveMyObject(MyAlarm : TAlarm)
var
MyStore: TJvAppXMLFileStorage;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.WritePersistent('', MyAlarm);
MyStore.Xml.SaveToFile('C:\MyAlarm.xml');
finally
MyStore.Free;
end;
end;
Restore a Object
uses
JvAppXMLStorage;
Procedure LoadMyObject(MyAlarm : TAlarm)
var
MyStore: TJvAppXMLFileStorage;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.FileName:='C:\MyAlarm.xml';
MyStore.Xml.LoadFromFile('C:\MyAlarm.xml');
MyStore.ReadPersistent('', MyAlarm);
finally
MyStore.Free;
end;
end;
UPDATE
If you need to persist more than one object to the XML file you must assign a path (unique id) to the WritePersistent and ReadPersistent methods.
See this example,
Multiple Persist
Procedure SaveMyObjects(MyObjects : Array of TComponent);
var
MyStore: TJvAppXMLFileStorage;
i : integer;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
for i := Low(MyObjects) to High(MyObjects) do
MyStore.WritePersistent(MyObjects[i].Name, MyObjects[i]); //In this case i use the name property of the component.
MyStore.Xml.SaveToFile('C:\Tools\MyAlarm.xml');
finally
MyStore.Free;
end;
end;
to save the components
SaveMyObjects([Button1,Button2,Edit1,Edit2]);
Multiple LOAD
Procedure LoadMyObjects(MyObjects:Array of TComponent);
var
MyStore : TJvAppXMLFileStorage;
i : integer;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.FileName:='C:\Tools\MyAlarm.xml';
MyStore.Xml.LoadFromFile('C:\Tools\MyAlarm.xml');
for i := Low(MyObjects) to High(MyObjects) do
MyStore.ReadPersistent(MyObjects[i].Name, MyObjects[i]);
finally
MyStore.Free;
end;
end;
To restore the properties
LoadMyObjects([Button1,Button2,Edit1,Edit2]);
Another option to load
Procedure LoadMyObjectById(Id:String;MyObject:TComponent); //using the id of the object
var
MyStore : TJvAppXMLFileStorage;
i : integer;
begin
MyStore:= TJvAppXMLFileStorage.Create(nil);
try
MyStore.FileName:='C:\Tools\MyAlarm.xml';
MyStore.Xml.LoadFromFile('C:\Tools\MyAlarm.xml');
MyStore.ReadPersistent(id, MyObject);
finally
MyStore.Free;
end;
end;
you must run it this way
LoadMyObjectById(Button1.Name,Button1); //Again using the Name property.
I hope this example will be useful ;)
You could persist the information in an XML or INI file locally. That doesn't require changing what TAlarm descends from. You would need to manually persist and restore all the properties that you wish to persist locally though. Shouldn't be that complicated.
If the server where you're supposed to be saving your data to is down, the best course of action is usually to cause the operation to fail and return an error. This way you don't need two separate sets of serialization code, both of which have to be kept in sync with each other, and a way to take your local data and upload it to the server once it's back up.
Plus, if your app depends on a remote server, it's likely that the user won't be able to do much with it offline anyway, so this isn't as bad of an idea as it may dound at first from a user-interface perspective.
I used a local database, an Access mdb file accessed thru ADO, with the same schema than the server. When connection recovers, I did a synchronization. But, nowadays, I have dropped this technique; wnen connection is lost or server is down, the application fails.