II. Fonctionnement interne▲
II-A. Unités de travail▲
Le moteur de conversion d'unités de mesure est implémenté dans l'unité ConvUtils ($DELPHI\source\rtl\common\ConvUtils.pas).
Tous les types d'unités de mesure sont déclarés dans des unités séparées. Delphi 6 fournit une unité qui déclare les types les plus standards : StdConvs.pas. On retrouve cette unité dans le même dossier que ConvUtils.
II-B. Types d'unités de mesure▲
Sachant que :
type
TConvFamily = type
Word
;
TConvType = type
Word
;
Delphi définit une unité de mesure à travers deux propriétés :
- ConvType de type TconvType ;
- ConvFamily de type TconvFamily.
ConvFamily représente la famille d'unités de mesure (exemple : les distances).
ConvType représente l'unité de mesure (exemple : les mètres).
À chaque famille et unité de mesure recensée correspond un numéro unique (TConvFamily et TConvType sont de type Word…) attribué lors de la déclaration.
II-C. Recensement des types d'unités de mesure▲
Qu'est-ce que le recensement et à quoi sert-il ?
Recenser une famille ou un type d'unité de mesure, c'est procéder à travers un mécanisme global, à la mémorisation des informations nécessaires à l'utilisation des ces données et de garder une référence sur ces données. En fait le recensement attribue en mémoire :
- un identifiant unique ;
- une description unique.
Et ceci à chaque famille et unité de mesure, pendant la durée d'exécution de l'application.
N'importe quelle unité, pour recenser une nouvelle famille ou unité de mesure, utilise le même mécanisme de recensement global (à condition d'insérer ConvUtils dans la clause uses). D'autres fonctions globales de ConvUtils permettent de prendre connaissance de la liste des familles et unités de mesure recensées globalement dans l'application.
Le recensement a lieu dans la section Initialization de l'unité, ici StdConvs.pas dont voici un extrait :
var
{ *************************************************************** }
{ Distance Conversion Units }
{ basic unit of measurement is meters }
cbDistance: TConvFamily;
duMicromicrons: TConvType;
duAngstroms: TConvType;
duMillimicrons: TConvType;
duMicrons: TConvType;
duMillimeters: TConvType;
duCentimeters: TConvType;
// …
// …
initialization
{ ********************************************************************* }
{ Distance's family type }
cbDistance := RegisterConversionFamily(SDistanceDescription);
{ Distance's various conversion types }
duMicromicrons := RegisterConversionType(cbDistance, SMicromicronsDescription, 1E-12
);
duAngstroms := RegisterConversionType(cbDistance, SAngstromsDescription, 1E-10
);
duMillimicrons := RegisterConversionType(cbDistance, SMillimicronsDescription, 1E-9
);
duMicrons := RegisterConversionType(cbDistance, SMicronsDescription, 1E-6
);
duMillimeters := RegisterConversionType(cbDistance, SMillimetersDescription, 0
.001
);
duCentimeters := RegisterConversionType(cbDistance, SCentimetersDescription, 0
.01
);
// …
// etc
Pour rappel, dès lors qu'une unité figure dans le projet, si elle comporte une clause Initialization, les instructions qui suivent cette clause sont exécutées dès le démarrage du programme.
La première instruction enregistre une nouvelle famille, celle des distances :
cbDistance := RegisterConversionFamily(SDistanceDescription);
Le recensement (RegisterConversionFamily) sert à attribuer un numéro à la variable cbDistance. Ce numéro sera unique parmi toutes les familles de conversion et valide durant l'exécution du programme. Comment fonctionne RegisterConversionFamily ?
function
RegisterConversionFamily(const
ADescription: string
): TConvFamily;
var
LFamily: TConvFamily;
begin
GConvFamilySync.BeginWrite;
try
if
DescriptionToConvFamily(ADescription, LFamily) then
RaiseConversionError(SConvDuplicateFamily, [ADescription]);
Inc(GLastConvFamily);
if
GLastConvFamily > Length(GConvFamilyList) - 1
then
SetLength(GConvFamilyList, GLastConvFamily + CListGrowthDelta);
Result := GLastConvFamily;
GConvFamilyList[Result] := TConvFamilyInfo.Create(Result, ADescription);
finally
GConvFamilySync.EndWrite;
end
;
end
;
Explications du code ci-dessus
- GConvFamilySync est une variable globale de type TMultiReadExclusiveWriteSynchronizer. Cela va permettre de protéger l'accès à la mémoire globale de façon à contraindre un accès exclusif en écriture et permettre un accès simultané en lecture. Ainsi on est certain que deux familles de conversion ne pourront obtenir le même numéro dans le cas où leur enregistrement serait exécuté à partir d'une application multithread. On indique à GConvFamilySync que l'ont souhaite accéder à la mémoire en écriture en appelant sa méthode BeginWrite.
- La fonction DescriptionToConvFamily vérifie qu'il n'existe pas déjà de famille portant la même description.
- Le dernier numéro attribué est incrémenté et la taille du tableau dynamique qui stocke la liste des familles est ajusté.
- Le résultat est affecté à Result, résultat du retour de fonction.
- Le tableau dynamique GConvFamily[] est un tableau d'objets de type TConvFamilyInfo.
TConvFamillyInfo - objet représentant une famille de conversions - est instancié avec pour paramètres : le numéro attribué à la famille et sa description.
- On indique à GConvFamilySync que l'on a fini d'écrire dans la mémoire globale en appelant sa méthode EndWrite.
Une fois que la famille de conversion est recensée, le recensement des unités de mesure de cette famille débute :
duMicromicrons := RegisterConversionType(cbDistance, SMicromicronsDescription, 1E-12
);
Le principe général est le même que pour les familles. Cependant, RegisterConversionType est une fonction redéclarée (directive overload) trois fois avec des paramètres différents pour chaque version :
function
RegisterConversionType(const
AFamily: TConvFamily; const
ADescription: string
; const
AFactor: Double
): TConvType; overload
;
function
RegisterConversionType(const
AFamily: TConvFamily;const
ADescription: string
; const
AToCommonProc, AFromCommonProc: TConversionProc): TConvType; overload
;
function
RegisterConversionType(AConvTypeInfo: TconvTypeInfo; out
AType: TConvType): Boolean
; overload
;
Donc selon les paramètres passés lors de l'appel à la fonction, c'est l'implémentation de telle ou telle version qui est exécutée. Dans le cas d'enregistrement de « duMicromicrons », c'est :
function
RegisterConversionType(const
AFamily: TConvFamily;
const
ADescription: string
; const
AFactor: Double
): TConvType;
var
LInfo: TConvTypeInfo;
begin
LInfo := TConvTypeFactor.Create(AFamily, ADescription, AFactor);
if
not
RegisterConversionType(LInfo, Result) then
begin
LInfo.Free;
RaiseConversionRegError(AFamily, ADescription);
end
;
end
;
Un objet représentant l'unité de mesure - de type TConvTypeFactor - est créé.
Il faut savoir que TConvTypeFactor hérite de TConvTypeInfo (fig. 7) et que cette classe implémente la conversion simple (cf. début du document). Nous verrons plus loin comment fonctionne cette classe lors de la conversion effective. Nous ne sommes encore que dans une phase préparatoire de déclaration et de recensement des types.
Un deuxième appel à RegisterConversionType est effectué, mais cette fois-ci avec des paramètres différents : l'objet TConvTypeFactor qui vient d'être créé et le résultat - encore vide - (paramètre de type out qui sera lu en sortie de fonction). Voici l'implémentation de cet appel RegisterConversionType(LInfo, Result) :
function
RegisterConversionType(AConvTypeInfo: TConvTypeInfo;
out
AType: TConvType): Boolean
;
begin
GConvTypeSync.BeginWrite;
try
Result := not
DescriptionToConvType(AConvTypeInfo.ConvFamily,
AConvTypeInfo.Description, AType);
if
Result then
begin
Inc(GLastConvType);
if
GLastConvType > Length(GConvTypeList) - 1
then
SetLength(GConvTypeList, GLastConvType + CListGrowthDelta);
AType := GLastConvType;
AConvTypeInfo.FConvType := AType;
GConvTypeList[AType] := AConvTypeInfo;
end
;
finally
GConvTypeSync.EndWrite;
end
;
end
;
On retrouve pour les unités de mesure un principe identique au recensement des familles :
- protection de la mémoire avec un TmultiReadExclusiveWriteSynchronizer ;
- vérification de non-existence de doublon de la description du type recensé ;
- mémorisation du type attribué dans un tableau dynamique dédié.
Si aucune erreur n'est rencontrée, le résultat de la fonction, de type booléen, est true. De plus, le numéro attribué au nouveau type est affecté au paramètre Atype (paramètre déclaré out).
L'appel d'origine était :
if
not
RegisterConversionType(LInfo, Result) then
// ...
Donc Result récupère la valeur de Atype dans Result, par là même clôturant l'exécution de la fonction.
Nous allons évoquer le côté persistent du recensement, c'est dire l'identification et la sauvegarde des informations utiles aux calculs de conversion : Recensement et Persistence !
II-D. Recensement et persistance▲
Nous avons vu que le recensement attribuait à chaque famille et unité de mesure un identifiant unique.
Ces identifiants sont valables jusqu'à la fermeture de l'application et il n'est pas garanti que lors de la prochaine exécution, les mêmes familles et unités possèdent le même identifiant (en réalité, il est possible de s'en assurer, à condition de respecter certaines règles que nous verrons plus loin).
II-D-1. Problématique▲
Cela pose un réel problème, car lorsque l'on travaille avec des données et que l'on souhaite les enregistrer dans un fichier ou un SGBD, voire les transférer via un webservice aux fins de calculs divers, il est très intéressant de pouvoir fournir le type d'unité de mesure correspondant à la donnée.
Mais lorsqu'on extrait une donnée d'un SGBD, comment savoir si cette donnée est exprimée en millimètres, en centimètres ou en mètres ? Il faut nécessairement stocker cette information (si on compte l'utiliser bien sûr).
Alors quelle information stocke-t-on ? La famille ou l'unité de mesure ou les deux ?
Selon les besoins de l'application, il faudra stocker au moins l'unité de mesure.
Les familles et unités de mesure sont déclarées de type word donc ce sont des entiers. Il suffit de prévoir dans la table de stockage un champ dédié pour stocker cet entier.
Il y a une autre possibilité, celle de stocker la description textuelle au lieu de l'identifiant, car il n'y a pas de doublon possible non plus, ce qui est vérifié lors du recensement. Mais autant la taille de la description importe peu au moment du recensement en mémoire (variable de type string), autant il faut prévoir la taille du champ de base de données qui va stocker cette information. D'une part cette taille n'est ni connue à l'avance ni limitée (ou alors le chef de projet doit imposer une taille maximale), et d'autre part, la taille de la base de données croîtrait beaucoup plus par rapport au stockage d'un simple entier.
Si vous désirez stocker les identifiants de type entier, les règles à respecter pour garantir l'obtention des mêmes identifiants à chaque exécution de l'application (et… pouvoir les faire correspondre à ceux qui ont été enregistrés dans un SGBD) sont simples.
Puisque le recensement a lieu dans la section initialization des unités et puisque le recensement attribue des identifiants incrémentés dans l'ordre d'exécution des instructions de recensement (RegisterConversionFamily et RegisterConversionType), nous devons faire en sorte que cet ordre ne soit jamais modifié. Il faut donc bien connaître l'ordre de chargement des unités et donc de l'exécution de leur clause initialization.
Delphi exécute en premier la section initialization de toutes les unités que l'unité en cours utilise, dans l'ordre d'énumération (uses unit1, unit2;), en commençant par les unités spécifiées dans la section interface suivies de celles de la section implémentation. Et seulement après, la section initialization de l'unité en cours est exécutée.
Même s'il est plusieurs fois fait référence à une même unité dans les clauses uses, la section initialization ne sera exécutée qu'une seule fois, car Delphi contrôle ce contexte d'exécution.
II-D-2. Recommandations▲
- Il est très recommandé d'externaliser tout recensement et toute opération de conversion dans une unité dédiée, ce qui veut dire que chaque fiche ou unité qui effectuera des conversions spécifiera dans sa clause uses l'unité dédiée.
- Cette unité doit implémenter le recensement des nouvelles familles et unités de mesure. L'ordre du recensement ne doit pas être modifié, les recensements existants ne doivent plus être supprimés une fois que des types ont déjà été stockés de façon persistante sur un support quelconque.
- Cette unité peut comporter dans sa clause uses : StdConvs si besoin et éventuellement d'autres unités spécifiques aux types de mesures et déjà implémentées et conserver l'ordre d'énumération de ces unités. On n'utilisera pas ces unités spécifiques dans les clauses uses des autres unités de l'application.
Si vous respectez ces règles, alors les identifiants attribués aux types resteront identiques à chaque exécution du programme puisqu’attribués séquentiellement et par incrémentation.
Si cela vous paraît lourd à gérer, on peut exploiter une autre solution.
II-D-3. L'identifiant de type GUID▲
Associer un identifiant unique à chaque type à recenser, dès la saisie du code, et le stocker dans un attribut que l'on aura ajouté dans une classe dérivée de TConvFamily (pour les familles) et TConvTypeInfo (pour les unités de mesure). Le meilleur moyen d'obtenir un identifiant unique est de générer un GUID. Dans l'EDI Delphi, on l'obtient avec la combinaison de touches Shift+Ctrl+G.
On insère l'identifiant produit au moment et à l'emplacement dans le code du recensement.
Voici un squelette d'implémentation dans lequel nous surchargerons le constructeur de la classe dérivée de TConvTypeFactor. Et nous créerons une fonction de recensement personnalisée appelant ce constructeur.
C'est lorsque l'on implémentera la demande de recensement que l'on spécifiera le GUID au clavier avec le raccourci Shift+Ctrl+G, exemple :
['{BF68A8A7-F026-4E4E-8C19-3F62BA406782}'
]
On dénuera cet identifiant des trois premiers et des trois derniers caractères pour conserver la partie unique et stockable dans un SGBD.
interface
TConvUniqueTypeFactor = class
(TConvTypeFactor)
public
GUID: TGUID;
constructor
Create(const
AFamily: TConvFamily; const
ADescription: string
;
AFactor: Double
; AGUID: TGUID);
end
;
var
uniqueType: TconvType;
implementation
{ TConvUniqueTypeFactor }
constructor
TConvUniqueTypeFactor.Create(const
AFamily: TConvFamily;
const
ADescription: string
; AFactor: Double
; AGUID: TGUID);
begin
inherited
Create(AFamily, ADescription, AFactor);
GUID := AGUID;
end
;
// Recensement
function
RegisterConvUniqueType(const
AFamily: TConvFamily;
const
ADescription: string
;AFactor: Double
;
AGUID: TGUID): TConvType; overload
;
var
UniqueInfo: TConvTypeInfo;
begin
UniqueInfo := TConvUniqueTypeFactor.Create(AFamily, ADescription,AFactor, AGUID);
if
not
RegisterConversionType(UniqueInfo, Result) then
begin
UniqueInfo.Free;
RaiseConversionRegError(AFamily, ADescription);
end
;
end
;
initialization
uniqueType := RegisterConvUniqueType(cbDecathlon,'unique'
,1
,StringtoGUID('DEBA952A-4B62-4D99-B4F2-344BC8559C83'
));
En utilisant cette méthode, vous avez une quasi-certitude d'attribuer des identifiants uniques lors du recensement, libérant du coup les contraintes d'ordre et d'impossibilité de suppression ultérieure. Stocker l'information de type de mesure de chaque donnée sur un support persistent revient ici à stocker le GUID respectif à ce type.
Si vous n'avez pas très bien saisi le code ci-dessus, vous trouverez plus loin des informations détaillées sur la création de conversions personnalisées.
Passons maintenant au cœur du fonctionnement de la conversion : la fonction Convert.