Conversions de mesures

Sylvain James

Conversions des mesures de toutes sortes avec Delphi

Le temps, l'espace, les distances, les poids, les quantités, et d'une manière générale la plupart des théories scientifiques n'ont été validées et ne sont reconnues qu'à travers l'emploi des mesures. La recherche scientifique n'a pas l'exclusivité ; la vie courante nous immerge dans le monde de la mesure sans que nous ne nous en rendions vraiment compte. Il n'est donc pas étonnant que l'on en retrouve fréquemment l'usage dans les applications informatiques. Qui dit mesure dit unité de mesure et dit conversions entre unités de mesure. Les conversions implémentées de manière anarchique dans les applications sont souvent source d'erreurs et leur maintenabilité est très relative. Pourtant c'est l'exemple même d'opération qui doit être à même d'être réutilisée dans n'importe quelle application. DELPHI 6 est arrivé avec un bon nombre de nouveautés dont certaines ont été passées sous silence ou n'ont pas été suffisamment remarquées. Au moment du passage réel à l'euro, il semble utile de faire découvrir les classes utilitaires de DELPHI 6 permettant de manipuler les unités de mesure. Que les débutants se rassurent, la mise en œuvre est très simple. Nous allons découvrir comment sont organisées la gestion et la conversion de mesures, nous présenterons quelques applications de démonstration, puis nous apprendrons à étendre le modèle de façon à pouvoir couvrir des besoins très précis.

- Titre : Conversions de mesures

- Auteur : Sylvain James

- Parution : 1 juillet 2002

- Mise à jour : 31 décembre 2019

- Licence : Copyright ® 2002 Sylvain James. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Généralités

- I-A. Familles de conversions

- I-B. Types de conversions

- I-C. Modes de conversions

- I-C-1. La conversion simple

- I-C-2. La conversion procédurale

- I-C-2-a. Création de l'ensemble de données

- I-C-2-b. Connexions aux contrôles visuels

- I-C-2-c. Réalisation de la conversion

- II. Fonctionnement interne

- II-A. Unités de travail

- II-B. Types d'unités de mesure

- II-C. Recensement des types d'unités de mesure

- II-D. Recensement et persistance

- II-D-1. Problématique

- II-D-2. Recommandations

- II-D-3. L'identifiant de type GUID

- III. Fonction Convert

- III-A. Première définition de Convert (conversion simple)

- III-B. Deuxième définition de Convert (conversion procédurale)

- IV. Création de conversions personnalisées

- IV-A. Conversion personnalisée simple

- IV-B. Conversion personnalisée procédurale

- IV-C. Création de la classe de conversion Décathlon

- V. Conversions et fonctions utilitaires

- V-A. Opérations arithmétiques

- V-B. Comparaisons et tests

- V-C. Listes de familles et d'unités de mesure

- V-D. Affichage numérique et textuel des mesures

- VI. Conclusion

I - Généralités

I-A - Familles de conversions

Les familles de conversion prises en charge en standard par Delphi 6 sont les suivantes :

- les distances (ex. : micromicron, mètre, mille nautique, pica, inch, etc.) ;

- les aires (ex. : are, hectare, mètre carré, yard carré, inch carré, etc.) ;

- les volumes (ex. : décimètre cube, hectomètre cubes, millilitre, décilitre, gallon, etc.) ;

- les masses (ex. : nanogramme, hectogramme, grain, pound, etc.) ;

- les températures (ex. : Celsius, kelvin, Fahrenheit, Réaumur, etc.) ;

- le temps (ex. : milliseconde, minute, semaine, décades, date julienne, etc.).

Le nombre d'unités de mesure déclaré par famille est non négligeable, il y a de fortes chances que vous découvriez pour la première fois un bon nombre de termes scientifiques…

I-B - Types de conversions

Un type de conversion est un sous-élément d'une famille de conversion.

Le micromicron, le mètre sont des types de conversion de la famille des distances.

Le nanogramme et le grain le sont aussi pour la famille des masses.

I-C - Modes de conversions

On distingue deux modes de conversion :

- la conversion simple ;

- la conversion procédurale.

I-C-1 - La conversion simple

La conversion simple s'appuie sur un facteur numérique. Exemple 1 miles = 1609,344 mètres donc la conversion s'effectue avec un facteur multiplicateur de 1,609344.

Nous allons créer une application permettant de convertir des mètres en miles et inversement.

Posez sur une form deux champs TLabeledEdit (onglet Supplément) qui nous permettront de saisir les valeurs, un pour les mètres et un pour les miles (fig. 1).

Figure 1

Dans la clause uses section implémentation de votre fiche, indiquez que vous utiliserez l'unité unités ConvUtils et StdConvs.

implementation

uses ConvUtils, StdConvs;

ConvUtils implémente les routines utilitaires de conversion, et StdConvs déclare les familles et types de mesures les plus courants (distances, températures, aires, volumes, etc.) accompagnés des facteurs multiplicateurs de conversion respectifs.

Nous allons implémenter les conversions Mètres <-> Miles dans les gestionnaires d'événement OnKeyDown des TLabeledEdit en interceptant la touche [Entrée].

procedure TfrmMetresMiles.edMetresKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
var
metres, miles: Double;
begin
if Key = VK_RETURN then
begin
metres := StrToFloat(edMetres.Text);
// Exécution de la conversion
miles := Convert(metres, duMeters, duMiles);
edMiles.Text := FloatToStr(miles);
end;
end;

La fonction globale Convert se charge de la conversion en spécifiant la valeur à convertir suivie des unités de mesure source et destination. Elle est déclarée dans l'unité ConvUtils comme suit :

function Convert(const AValue: Double; const AFrom, ATo: TConvType): Double;

Le résultat est de type Double. Elle attend en paramètre :

- la valeur à convertir (de type Double) ;

- le type de l'unité de mesure de la valeur à convertir (exemple : duMeters pour Mètres) ;

- le type de l'unité de mesure résultat (exemple : duMiles pour Miles).

Pour réaliser la conversion inverse, implémentez le gestionnaire OnKeyDown du TLabeledEdit respectif aux Miles, et appelez la routine de conversion Convert en inversant les types d'unités de mesure pour les paramètres AFrom et ATo :

Pour convertir des mètres en miles, nous avions la ligne de code suivante :

miles := Convert(metres, duMeters, duMiles);

On implémente l'opération inverse dans le gestionnaire OnKeyDown du TLabeledEdit respectif à la saisie des miles :

metres := Convert(miles, duMiles, duMeters);

Avantage de l'opération : nous avons réalisé une conversion de valeur entre deux unités de mesure à l'aide d'une seule ligne de code sans se soucier des unités ni des facteurs de conversion.

Jusqu'à présent, nous avons vu des conversions mettant en jeu une seule famille de conversions, les distances. Mais plusieurs cas existent où deux familles interviennent dans la mesure. Par exemple, les kilomètres/heure, les litres/minute, etc.

Nous allons ajouter des éléments à notre application (fig. 2) de façon à calculer une conversion d'un temps exprimé en secondes pour parcourir une distance en mètres, et obtenir la vitesse correspondante en kilomètres par heure :

Figure 2

43"18 est le record du monde du 400 m plat détenu par Michaël Johnson (1999 - Séville).

Dans le code, nous allons intercepter la touche Entrée sur le TLabeledEdit de saisie du temps, et implémenter la conversion :

procedure TfrmMetresMiles.edTempsKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
var
distance, temps, vitesse: double;
begin
if Key <> VK_RETURN then exit;
distance := StrToFloat(edDistance.Text);
temps := StrToFloat(edTemps.Text);
// La vitesse est exprimée en m/s et convertie en km/h
vitesse := Convert(distance/temps, duMeters, tuSeconds, duKilometers, tuHours);
edVitesse.Text := FloatToStr(vitesse);
end;

Nous avons encore fait appel à la fonction Convert, mais cette fois avec des paramètres différents. Convert est en effet redéfini deux fois dans l'unité ConvUtils.

Voyons maintenant le deuxième mode de conversion…

I-C-2 - La conversion procédurale

La conversion procédurale recherche un résultat qui n'est plus lié à un simple facteur de conversion, mais qui est pris en charge par une fonction de conversion implémentée par le développeur (exécution de méthodes d'un objet, récupération de données dans un SGBD, etc.).

Dans une application se chargeant d'effectuer une conversion francs/euros, nous aimerions que la routine de conversion se charge en plus d'appliquer l'arrondi bancaire.

Nous allons créer une application base de données locale de 1er niveau, utilisant un ClientDataSet qui servira de table locale pour stocker des enregistrements. Chaque enregistrement stockera un tarif en euros pour un article donné. Nous saisirons les montants en euros, mais pour un temps indéterminé nous souhaitons pouvoir afficher le montant respectif en francs. À cet effet, nous utiliserons un champ calculé pour l'affichage des montants en francs. De fait, lorsque nous n'aurons plus besoin de visualiser les prix en francs, il sera aisé de supprimer le champ calculé, sans pour autant modifier la structure de la table.

La conversion en elle-même est aussi simple que pour le premier exemple, puisque la conversion francs/euros ne tient qu'à une simple opération de multiplication ou de division réalisée par la fonction Convert (facteur multiplicateur de 6.55957).

Mais cela ne nous suffit pas, nous aimerions effectuer un arrondi bancaire sur le résultat, et surtout que le calcul de cet arrondi soit pris en charge par la routine de conversion elle-même.

L'unité EuroConv livrée dans les démos effectue cette opération en surchargeant certaines routines de ConvUtils de façon à personnaliser la conversion. En l'occurrence, après conversion classique d'une monnaie vers l'autre, le résultat est passé à la fonction SimpleRound qui effectue l'arrondi bancaire. On a bien ici une spécialisation de la conversion de base :

Result := AValue / Factor;

Revenons à notre application et dans la clause uses section implémentation de votre fiche, ajoutez une référence à l'unité EuroConv.

var
frmEuroFranc: TfrmEuroFranc;

implementation

uses ConvUtils, EuroConv;

Attention l'unité EuroConv est située dans le dossier $DELPHI\Demos\ConvertIt, ce chemin doit être connu dans les options du projet (Répertoires/Chemin de Recherche) ou bien copiez-le dans le même dossier que votre application. L'unité EuroConv est un ajout de Borland au framework de conversion initial, en guise d'exemple de personnalisation. Elle recense toutes les monnaies liées à l'euro et permet donc de réaliser les conversions dans tous les sens de monnaie possibles.

I-C-2-a - Création de l'ensemble de données

Ensuite, disposez un TClientDataSet sur la fiche, double-cliquez dessus pour ouvrir l'éditeur de champs, ensuite un clic droit sur l'éditeur donne accès au menu [Nouveau Champ] (fig. 3).

Figure 3

Ajoutez les champs suivants :

Nom du champ

Type de champ

Type de donnée

Valeur

IDTarif

Données

AutoInc

LibelleTarif

Données

String

100

TarifsEuro

Données

Float

TarifsFrancs

Calculé

Float

Ensuite, à l'aide du menu contextuel sur le ClientDataSet, sélectionnez [Créez un ensemble de données].

I-C-2-b - Connexions aux contrôles visuels

Disposez un DBGrid, un DBNavigator et des champs DB d'édition, tous reliés via un Datasource à l'ensemble de données client ci-dessus (fig. 4).

Figure 4

À l'ouverture de l'application, nous activerons le ClientDataSet et le désactiverons à la fermeture.

procedure TfrmEuroFranc.FormCreate(Sender: TObject);
begin
cdsTarif.Active := True;
end;

procedure TfrmEuroFranc.FormDestroy(Sender: TObject);
begin
cdsTarif.Active := False;
end;

I-C-2-c - Réalisation de la conversion

Puisque c'est un champ calculé qui affichera le montant en francs respectif au montant saisi en euros, nous implémenterons la conversion en gérant l'événement OnCalcField du ClientDataSet :

procedure TfrmEuroFranc.cdsTarifCalcFields(DataSet: TDataSet);
begin
TarifFrancs.Value := Convert(TarifEuros.Value, euEUR, euFFR);
end;

euEUR et euFFR sont des types d'unités de mesure déclarés dans l'unité EuroConv, et représentent respectivement les unités de mesure source et destination nécessaires à la conversion.

Avantage de l'opération : comme pour le premier exemple, un appel à la fonction Convert a suffi pour obtenir une conversion de données. Cependant cette dernière ici élargissait l'opération de base en ajoutant le calcul de l'arrondi bancaire.

On peut en conclure que nous bénéficions d'un ensemble cohérent et générique, quelle que soit la complexité de l'opération de conversion. Nous allons à présent étudier comment fonctionne ce framework de conversion et comment implémenter des conversions personnalisées non disponibles dans l'unité StdConvs et EuroConv.

Nous allons à présent découvrir le fonctionnement interne, à savoir comment les conversions sont réalisées par Delphi (et Kylix).

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.

Figure 5

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.

Figure 6

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.

Figure 7

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.

III - Fonction Convert

C'est la fonction globale Convert qui réalise la conversion. La valeur à convertir est de type double et le résultat fourni est également de type double.

On peut faire appel à la fonction Convert dès lors que les familles et unités de mesure sont recensées.

Nous allons reprendre le premier exemple pour comprendre le fonctionnement. Rappelons que la fonction Convert est définie deux fois avec des paramètres différents (directive overload) :

Figure 8

III-A - Première définition de Convert (conversion simple)

La ligne de code suivante :

miles := Convert(metres, duMeters, duMiles);

appelle la première définition de Convert, soit une conversion simple. Voyons le détail du code exécuté :

function Convert(const AValue: Double; const AFrom, ATo: TConvType): Double;
var
LFromTypeInfo, LToTypeInfo: TConvTypeInfo;
begin
if not GetConvInfo(AFrom, ATo, LFromTypeInfo, LToTypeInfo) then
RaiseConversionError(SConvIncompatibleTypes2,
[ConvTypeToDescription(AFrom),
ConvTypeToDescription(ATo)]);
Result := LToTypeInfo.FromCommon(LFromTypeInfo.ToCommon(AValue));
end;

On commence par un appel à GetConvInfo. On passe en paramètre le type de l'unité de mesure source (AFrom) et destination (ATo) et on obtiendra en retour les objets TConvInfo (fig. 9) respectifs (dans LFromTypeInfo et LToTypeInfo). Cela va permettre, à partir des références (nombres entiers) des unités recensées de retrouver les objets de type TConvInfo respectifs, car ce sont eux qui implémentent la conversion.

Figure 9

Si GetConvInfo a réussi, alors le calcul de la conversion réside dans l'appel des méthodes FromCommon et ToCommon des objets TConvTypeInfo précédemment retrouvés.

Ce sont les méthodes de TConvTypeFactor (descendant de TConvInfo) qui sont appelées, car les types d'unités de mesure metres et miles ont été recensés avec un facteur multiplicateur (fig. 10).

Figure 10

FFromTypeInfo.ToCommon(AValue) est calculé en premier, voici l'implémentation VCL :

function TConvTypeFactor.ToCommon(const AValue: Double): Double;
begin
Result := AValue * FFactor;
end;

Et le résultat est ensuite passé à LToTypeInfo.FromCommon(le résultat de 3.) :

function TConvTypeFactor.FromCommon(const AValue: Double): Double;
begin
Result := AValue / FFactor;
end;

Le double appel :

Result := LToTypeInfo.FromCommon(LFromTypeInfo.ToCommon(AValue));

correspond dans notre exemple au calcul :

Resultat := AValue * Facteur mètres / Facteur miles;

La VCL convertit la mesure initiale vers l'unité de mesure commune pour ensuite la convertir vers l'unité de mesure finale ce qui est logique puisque toutes les unités de mesure possèdent un facteur de conversion lié à l'unité de mesure commune.

Lors du recensement, la VCL prend connaissance du facteur de conversion entre toutes les unités de mesure recensées et l'unité de mesure commune de la même famille. FFactor est un attribut privé (accédé par la propriété Factor) qui mémorise le facteur de conversion de l'unité de mesure respective par rapport à l'unité de mesure commune (unité dont le facteur = 1).

Figure 11

La fig. 11 explique comment fonctionne la VCL pour convertir une mesure vers une autre en spécifiant qu'elle passe par une étape intermédiaire : la conversion vers l'unité de mesure commune.

III-B - Deuxième définition de Convert (conversion procédurale)

Le fonctionnement dans le cadre d'une conversion procédurale est quasi identique. Voyons avec un exemple de conversion performance --> points de l'épreuve de perche du décathlon (que l'on retrouvera en détail plus loin).

duPerfPerche et duPointDecation sont des pointeurs de fonctions de type TConversionProc dont la déclaration est la suivante :

TConversionProc = function(const AValue: Double): Double;

L'exécution de la conversion s'effectuera comme suit :

points := Convert(perf, duPerfPerche, duPointDecathlon);

La fonction Convert est appelée comme précédemment, une différence subsiste au moment de

Result := LToTypeInfo.FromCommon(LFromTypeInfo.ToCommon(AValue));

Le type duPerfPerche a été recensé en créant un objet non plus de type TConvTypeFactor, mais de type TConvTypeProcs (voir fig. 12). La différence est que les fonctions ToCommon et FromCommon ne sont pas implémentées dans cette classe, mais déléguées à des fonctions fournies par le développeur (stockées dans les propriétés privées FToCommonProc et FFromCommonProc au moment du recensement) :

decaPerche := RegisterConversionType(cbDecathlon, 'Perf Perche', PerfPercheToPoints, PointsToPerfPerche);

Ici, FToCommonProc référence la fonction PerfPercheToPoints et FFromCommonProc référence la fonction PointsToPerfPerche.

On retrouve la redirection d'appel à ces fonctions dans l'implémentation de ToCommon et de FromCommon :

function TConvTypeProcs.ToCommon(const AValue: Double): Double;
begin
Result := FToCommonProc(AValue);
end;

function TConvTypeProcs.FromCommon(const AValue: Double): Double;
begin
Result := FFromCommonProc(AValue);
end;

Diagramme de classes :

Figure 12

Nous avons maintenant suffisamment de connaissances pour créer un système de conversions personnalisées.

IV - Création de conversions personnalisées

Bien que l'unité StdConvs.pas recense un bon nombre de familles et d'unités de mesure, il est possible que vous ayez besoin de compléter cette collection.

IV-A - Conversion personnalisée simple

Imaginons un instant que suite à une nouvelle étude sur le continent imaginaire : Atlantide, il ait été découvert que l'unité de mesure atlante était le « pied atlante » et que celui-ci mesurait par exemple 18,36 cm.

Mesure totalement imaginaire…, en passant, si les mythes et légendes vous intéressent, allez faire un tour sur www.mythorama.com dont le webmaster est un delphinaute, vous ne serez pas déçus.

Nous allons créer une nouvelle application et recenser la nouvelle unité de mesure atlante dans la famille des distances.

Ci-dessous le source de l'unité de la fiche principale :

unit uCustomConv;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ConvUtils, StdConvs;

type
TfrmCustomConvert = class(TForm)
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;

var
frmCustomConvert: TfrmCustomConvert;
duPiedAtlante: TConvType;

implementation

{$R *.dfm}

initialization
duPiedAtlante := RegisterConversionType(cbDistance,'Pieds Atlantes', 0.1836);
end.

Cette fois-ci, nous ajoutons les références aux unités de conversion (ConvUtils et StdConvs) dans la clause uses de la section interface et non implémentation. Pourquoi ? Car nous avons déclaré la nouvelle variable duPiedAtlante avant la clause implémentation de façon à ce que cette variable soit éventuellement accessible à d'autres unités.

Le facteur de conversion est calculé par rapport à l'unité de la famille des distances qui a un facteur 1, ici le mètre. Nous le vérifions dans l'unité StdConvs, extrait :

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);
duDecimeters := RegisterConversionType(cbDistance, SDecimetersDescription, 0.1);
duMeters := RegisterConversionType(cbDistance, SMetersDescription, 1);

On peut également noter que le dérecensement, bien que prévu et exécuté logiquement dans la clause finalization, n'est pas obligatoire. Le cas échéant, on aurait :

initialization
duPiedAtlante := RegisterConversionType(cbDistance,'Pieds Atlantes', 0.1836);
finalization
UnregisterConversionType(duPiedAtlante);
end.

Maintenant, passons à la pratique et posons deux TLabeledEdit de façon à ce que l'on puisse implémenter une conversion des mètres vers des pieds atlantes et inversement (dans les événements OnKeyDown des TLabeledEdit) :

procedure TfrmCustomConvert.edMetresKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
metres, piedsatlantes: double;
begin
if key <> VK_RETURN then exit;
metres := StrToFloat(edMetres.Text);
piedsatlantes := Convert(metres, duMeters, duPiedAtlante);
edPiedsAtlantes.Text := FloatToStr(piedsatlantes);
end;

Le code dans le TLabeledEdit des « Pieds Atlantes » est sensiblement le même en intervertissant les variables.

IV-B - Conversion personnalisée procédurale

Nous allons continuer avec des conversions personnalisées un peu plus compliquées. Écartons-nous du domaine scientifique et dirigeons-nous du côté du domaine sportif (quoique le sport prenne de plus en plus un caractère scientifique, dans le bon comme dans le mauvais…) et plus précisément des cotations de performances. En athlétisme par exemple, les performances de chacune des dix épreuves du décathlon sont cotées à la table des épreuves combinées de l'IAAF (International Association of Athletics Federations). À une performance correspond un nombre de points.

Des formules existent pour trouver les points correspondants à une performance.

Nous allons prendre l'exemple du saut à la perche et la formule de calcul des points dédiée :

points = 0.2797.((perf en cm - 100)^1.35)

Nous allons recenser une nouvelle famille de conversion : cbDecathlon ainsi que le premier type : decaPerche. Nous continuons avec la même unité que pour l'exemple précédent.

La déclaration de la famille cbDecathlon et du type duPerche :

var
frmCustomConvert: TfrmCustomConvert;
duPiedAtlante: TConvType;
cbDecathlon: TConvFamily;
decaPerche: TconvType;
decaPoint: TConvType;

implementation
//...
//...

initialization
duPiedAtlante := RegisterConversionType(cbDistance,'Pieds Atlantes', 0.1836)

cbDecathlon := RegisterConversionFamily('Décathlon');
decaPerche := RegisterConversionType(cbDecathlon, 'Perche', PerfPercheToPoints, PointsToPerfPerche);
decaPoint := RegisterConversionType(cbDecathlon, 'Points', 1);

Pour decaPoint, l'unité du point de cotation est 1 donc rien de bien compliqué.

Concernant decaPerche, au lieu de passer en paramètre un facteur multiplicateur, nous avons passé deux variables de type TConversionProc qui en fait pointeront sur les fonctions respectives chargées de la conversion.

Rappelons la définition de TConversionProc :

TConversionProc = function(const AValue: Double): Double;

PerfPercheToPoints et PointsToPerfPerche doivent donc être deux fonctions de ce type. Nous les implémentons dans la section… implémentation.

implementation

{$R *.dfm}

uses Math;

function PerfPercheToPoints(const AValue: double): double;
begin
result := trunc(0.2797 * (Power(AValue - 100, 1.35)));
end;

function PointsToPerfPerche(const AValue: double): double;
begin
result := 0; // ??? à calculer... aie faut que je révise mes maths :-(
end;

Comme pour les autres exemples, pour calculer les points d'une perf à la perche en cm, vous implémenterez la conversion :

points := Convert(perf, decaPerche, decaPoint);

L'inconvénient de cette méthode est que l'on doit implémenter deux fonctions de conversion pour chaque épreuve du décathlon ce qui est assez lourd sachant que les formules respectives sont quasi identiques et paramétrables.

Voici la liste des formules pour les épreuves du décathlon :

100m

Points = 25.4347.((18 - temps en sec)^1.81)

Longueur

Points = 0.14354.((perf en cm - 220)^1.4)

Poids

Points = 51.39.((perf en m - 1.5)^1.05)

Hauteur

Points = 0.8465.((perf en cm - 75)^1.42)

400m

Points = 1.53775.((82 - temps en sec)^1.81)

110m haies

Points = 5.74352.((28.5 - temps en sec)^1.92)

Disque

Points = 12.91.((perf en m - 4)^1.1)

Perche

Points = 0.2797.((perf en m - 100)^1.35)

Javelot

Points = 10.14.((perf en cm - 7)^1.08)

1500m

Points = 0.03768.((480 - temps en sec)^1.85)

On constate que la structure est similaire pour toutes les formules :

points = const1 . ( ( perf - const2 ) ^ const3 )

et que selon la famille de la performance (Temps ou Distance), le signe des paramètres perf et const2 sont inversés :

points = const1 . ( ( const2 - perf ) ^ const3 )

D'autre part, la perf est exprimée en centimètres lorsqu'il s'agit d'un saut et en mètres lorsqu'il s'agit d'un lancer. Pour simplifier, nous indiquerons à l'utilisateur dans quelle unité de mesure il doit saisir la performance.

Il serait intéressant de créer une classe générique, qui permettrait d'implémenter une seule fois le calcul de conversion perf --> points quelle que soit l'épreuve et d'implémenter une méthode de recensement de cette classe en prenant en compte les paramètres propres à chaque épreuve. C'est ce que nous allons faire dans la rubrique suivante.

IV-C - Création de la classe de conversion Décathlon

Dans ce cas, nous devons diriger les méthodes de conversion ToCommon et FromCommon vers des fonctions implémentant nos formules de calcul dédiées utilisant les paramètres const1, const2 et const3. Nous ajoutons également une variable qui permettra d'indiquer si la valeur à convertir fait partie de la famille des distances ou de la famille des temps, car la formule de calcul diffère en fonction de cette particularité.

Nous allons créer une nouvelle application intégrant une classe héritant de TConvTypeFactor que l'on appellera TConvTypeDecathlon puis surcharger l'implémentation du constructeur et des fonctions ToCommon et FromCommon.

Interface :

TConvTypeDecathlon = class(TConvTypeFactor)
private
fConst1,
fConst2,
fConst3: double;
fTypePerf : TConvFamily;
public
constructor Create(const AFamily: TConvFamily; const ADescription: string;
Const1, Const2, Const3: double; TypePerf : TConvFamily);
function ToCommon(const AValue: Double): Double; override;
function FromCommon(const AValue: Double): Double; override;
end;

Implémentation :

constructor TConvTypeDecathlon.Create(const AFamily: TConvFamily;
const ADescription: string; Const1, Const2, Const3: double;
TypePerf : TConvFamily);
begin
inherited Create(AFamily, ADescription, 1);
fConst1 := Const1;
fConst2 := Const2;
fConst3 := Const3;
fTypeEpreuve := TypeEpreuve;
end;

function TConvTypeDecathlon.ToCommon(const AValue: Double): Double;
begin
case fTypePerf of
cbDistance:
result := trunc(fConst1 * (Power(AValue - fConst2, fConst3)));
cbTime:
result := trunc(fConst1 * (Power(fConst2 - AValue, fConst3)));
end;
end;

function TConvTypeDecathlon.FromCommon(const AValue: Double): Double;
begin
// à vérifier
Result := Avalue ;
end;

La classe générique de conversion pour toutes les épreuves du décathlon est prête. Il reste à recenser la famille et chacune des épreuves du décathlon.

Nous recenserons chaque épreuve du décathlon en spécifiant les paramètres const1, const2 et const3 et le type de la performance respective (temps ou distance). Nous implémenterons donc les fonctions de recensement adaptées.

Interface :

function RegisterDecathlonConversionType(const AFamily: TConvFamily;
const ADescription: string;
Const1, Const2, Const3: double; TypePerf: TConvFamily): TConvType; overload;

function RegisterDecathlonConversionType(const AFamily: TConvFamily;
const ADescription: string; const AToCommonProc,
AFromCommonProc: TConversionProc;
Const1, Const2, Const3: double; TypePerf: TConvFamily): TConvType; overload;

Implémentation :

function RegisterDecathlonConversionType(const AFamily: TConvFamily;
const ADescription: string;
Const1, Const2, Const3: double; TypePerf: TConvFamily): TConvType; overload;
var
EpreuveInfo: TConvTypeInfo;
begin
EpreuveInfo := TConvTypeDecathlon.Create(AFamily, ADescription,
Const1, Const2, Const3, TypePerf);
if not RegisterConversionType(EpreuveInfo, Result) then
begin
EpreuveInfo.Free;
RaiseConversionRegError(AFamily, ADescription);
end;
end;

function RegisterDecathlonConversionType(const AFamily: TConvFamily;
const ADescription: string; const AToCommonProc,
AFromCommonProc: TConversionProc): TConvType; overload;
begin
Result := RegisterConversionType(AFamily, ADescription,
AToCommonProc, AFromCommonProc);
end;

Notre classe et nos méthodes de recensement sont prêtes, déclarons la famille du décathlon et les épreuves en tant que variables globales de l'unité et recensons-les dans la clause initialization de l'unité :

unit uDecathlon;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ConvUtils;

type
// ...

var
frmDecathlon: TfrmDecathlon;

cbDecathlon: TConvFamily;
deca100m,
decaLongueur,
decaPoids,
decaHauteur,
deca400m,
deca110haies,
decaDisque,
decaPerche,
decaJavelot,
deca1500m: TConvType;
decaPoint: TConvType;

implementation

// ...

initialization
cbDecathlon := RegisterConversionFamily('Decathlon');
deca100m := RegisterDecathlonConversionType(cbDecathlon, '100M', 25.4347, 18, 1.81, cbTime);
decaLongueur := RegisterDecathlonConversionType(cbDecathlon, 'LONGUEUR', 0.14354, 220, 1.4, cbDistance);
decaPoids := RegisterDecathlonConversionType(cbDecathlon, 'POIDS', 51.39, 1.5, 1.05, cbDistance);
decaHauteur := RegisterDecathlonConversionType(cbDecathlon, 'HAUTEUR', 25.4347, 18, 1.81, cbDistance);
deca400m := RegisterDecathlonConversionType(cbDecathlon, '400M', 25.4347, 18, 1.81, cbTime);
deca110haies := RegisterDecathlonConversionType(cbDecathlon, '110MHAIES', 5.74352, 28.5, 1.92, cbTime);
decaDisque := RegisterDecathlonConversionType(cbDecathlon, 'DISQUE', 12.91, 4, 1.1, cbDistance);
decaPerche := RegisterDecathlonConversionType(cbDecathlon, 'PERCHE', 0.2797, 100, 1.35, cbDistance);
decaJavelot := RegisterDecathlonConversionType(cbDecathlon, 'JAVELOT', 10.14, 7, 1.08, cbDistance);
deca1500m := RegisterDecathlonConversionType(cbDecathlon, '1500M', 0.03768, 480, 1.85, cbTime);
decaPoint := RegisterConversionType(cbDecathlon, 'POINTS', 1);
end.

Ensuite nous disposons un couple de TLabeledEdit pour saisir la performance et d'un TLabel pour afficher le nombre de points, et ce pour chaque épreuve (fig. 13). Les saisies de performances se feront en mètres pour les distances et en secondes pour les temps, pour des raisons de simplicité.

Il aurait été judicieux de créer une frame (TFrame) pour représenter chaque épreuve ainsi que de fournir un composant d'édition des performances affichant un masque de saisie en fonction de l'épreuve, ce que nous écartons afin de nous concentrer plutôt sur la conversion.

Nous implémentons la conversion de la performance vers le nombre de points correspondant lorsque l'on quitte le champ d'édition, donc en gérant l'événement OnExit. Voici un extrait pour les trois premières épreuves :

procedure TfrmDecathlon.ed100mExit(Sender: TObject);
begin
lbl100m.Caption := FloatToStr(Convert(StrToFloat(ed100m.Text), deca100m, decaPoint));
end;

procedure TfrmDecathlon.edLongueurExit(Sender: TObject);
begin
lblLongueur.Caption :=FloatToStr(Convert(StrToFloat(edLongueur.Text), decaLongueur, decaPoint));
end;

procedure TfrmDecathlon.edPoidsExit(Sender: TObject);
begin
lblPoids.Caption := FloatToStr(Convert(StrToFloat(edPoids.Text), decaPoids, decaPoint));
end;

// ...

Figure 13

Les points correspondant aux performances sont maintenant calculés avec un seul appel à la fonction générique Convert. Notre objectif est atteint.

Bien sûr il nous reste du travail quant à l'élégance du code, et aussi au niveau du traitement des exceptions ici volontairement éludé.

Le framework de conversions Delphi propose aussi quelques fonctions utilitaires. Découverte…

V - Conversions et fonctions utilitaires

L'unité ConvUtils fournit plusieurs fonctions globales permettant de manipuler les données dont l'unité de mesure est recensée.

V-A - Opérations arithmétiques

Fonction ConvUnitAdd

function ConvUnitAdd(const AValue1: Double; const AType1: TConvType; const AValue2: Double; const AType2, AResultType: TConvType): Double;

Additionne deux mesures. On spécifie le type de mesure pour chacune des valeurs ainsi que le type de mesure du résultat.

Fonction ConvUnitDiff

function ConvUnitDiff(const AValue1: Double; const AType1: TConvType; const AValue2: Double; const AType2, AResultType: TConvType): Double;

Même chose, mais la fonction effectue une soustraction.

On peut citer également les fonctions ConvUnitInc et ConvUnitDec qui permettent d'incrémenter et de décrémenter une mesure dans le type de mesure spécifié.

V-B - Comparaisons et tests

Fonction ConvUnitWithinPrevious

function ConvUnitWithinPrevious(const AValue, ATest: Double;   const AType: TConvType; const AAmount: Double; const AAmountType: TConvType): Boolean;

Permet de vérifier si la valeur AValue est inférieure à la valeur ATest d'au plus la valeur AAmount. Nous précisons au passage le type d'unité de mesure dans lequel les valeurs sont exprimées.

Fonction ConvUnitWithinNext

function ConvUnitWithinNext(const AValue, ATest: Double; const AType: TConvType; const AAmount: Double; const AAmountType: TConvType): Boolean;

Idem que pour ConvUnitWithinPrevious, mais on teste si AValue est supérieur à ATest.

Fonction ConvUnitCompareValue

function ConvUnitCompareValue(const AValue1: Double; const AType1: TConvType; const AValue2: Double; const AType2: TConvType): TValueRelationship;

Compare deux valeurs, chacune spécifiée avec son unité de mesure respective (de la même famille), et renvoie un résultat de type TValueRelationship. TValueRelationship comporte 3 valeurs possibles :

- LessThanValue (= -1)

- EqualsValue (= 0)

- GreaterThanValue (=1)

Fonction ConvUnitSameValue

function ConvUnitSameValue(const AValue1: Double; const AType1: TConvType; const AValue2: Double; const AType2: TConvType): Boolean;

Cette fonction est un peu plus basique que la précédente, car elle renvoie seulement true ou false selon que les deux valeurs passées en argument sont égales ou non.

Fonction CompatibleConversionTypes

function CompatibleConversionTypes(const AFrom, ATo: TConvType): Boolean;

Vérifie si deux types font partie de la même famille. Cela permet d'éviter de demander une conversion qui échouerait en raison de cette incompatibilité.

V-C - Listes de familles et d'unités de mesure

Procédure GetConvTypes

procedure GetConvTypes(const AFamily: TConvFamily; out ATypes: TconvTypeArray);

Récupère dans le paramètre ATypes la liste des types de mesure de la famille passée en paramètre (AFamily). TConvTypeArray est un tableau dynamique de TConvType soit un tableau d'entiers.

Procédure GetConvFamilies

procedure GetConvFamilies(out AFamilies: TConvFamilyArray);

Récupère la liste des familles recensées dans une variable tableau dynamique.

V-D - Affichage numérique et textuel des mesures

Fonction StrToConvUnit

function StrToConvUnit(AText: string; out AType: TConvType): Double;

Fonction intéressante qui permet de convertir une mesure exprimée textuellement en valeur de type TConvType. Par exemple :

StrToConvUnit('100 mètres', MonNouveauType)

parse la chaîne passée en paramètre et instancie un objet TConvType avec 'mètres' pour description et 100 pour valeur.

Fonction ConvUnitToStr

function ConvUnitToStr(const AValue: Double; const AType: TConvType): string;

C'est l'inverse qui se produit : à partir un objet TConvtype passé en paramètre et une valeur, on obtient une chaîne descriptive. Exemple :

ConvUnitToStr(100, duMeters)

renvoie ‘100 Mètres'

VI - Conclusion

Nous avons fait le tour du système de conversion d'unités de mesure sous Delphi 6, Kylix 2 et aussi, aurait-il fallu ajouter C++Builder 6 puisque, rappelons-le, ce dernier intègre la VCL (Visual Component Library), bibliothèque phare d'objets de Borland.
J'espère que vous aurez appris suffisamment d'éléments pour être capable d'intégrer vos systèmes de conversions dans vos applications. Si vous avez des questions, n'hésitez pas, et postez-les sur le forum Delphi de Developpez, tout le monde en profitera du coup.
Vous gagnerez un petit peu de temps en téléchargeant les sources ici.

Bon courage !