Робота з різними одиницями вимірювання однакового типу та їх конвертація

Доброго часу доби. Зіткнувся з цікавою завданням у великому проекті, де багато розрахунків різних відстаней. При тому, що дані збираються з різних джерел, вони можуть бути в різних одиницях — там метри, там міліметри. Встежити за всім цим важко, коли обчислення розкидані всюди. А якщо об'явлена змінна, то найчастіше тільки автору достеменно відомо, в яких вона одиницях, так як коментарів в коді майже немає. А автор звільнився/забув/пішов у запій.

Напрошується рішення описати кожну одиницю окремим типом, наприклад так:

type
TSizeMeter = single;
TSizeMilliMeter = single;

Цим ми збільшили читаність коду. Але як уберегтися від помилок конвертації? Адже ці типи сумісні, і їх каст не дасть ні помилки, ні ворнинга (див. тут). Я так і не знайшов спосіб виводити ворнинг при касти типів, але ми можемо написати автоматичну конвертацію, якщо такий каст відбувається. Ось найпростіший приклад зі складанням і відніманням:

interface

type
TSizeMeter = record
value:single;
const units='m';
class operator Add(a, b: TSizeMeter): TSizeMeter;
class operator Subtract(a, b: TSizeMeter): TSizeMeter;
class operator Implicit(a: single): TSizeMeter;
class operator Implicit(a: TSizeMeter): single;
end;

TSizeMiliMeter = record
value:single;
const units='mm';
class operator Add(a, b: TSizeMiliMeter): TSizeMiliMeter;
class operator Subtract(a, b: TSizeMiliMeter): TSizeMiliMeter;
class operator Implicit(a: single): TSizeMiliMeter;
class operator Implicit(a: TSizeMiliMeter): single;
class operator Implicit(a: TSizeMiliMeter): TSizeMeter;
class operator Implicit(a: TSizeMeter): TSizeMiliMeter;
end;

implementation

class operator TSizeMeter.Add(a, b: TSizeMeter): TSizeMeter;
begin
result.value:=a.value+b.value;
end;

class operator TSizeMeter.Subtract(a, b: TSizeMeter): TSizeMeter;
begin
result.value:=a.value-b.value;
end;

class operator TSizeMeter.Implicit(a: single): TSizeMeter;
begin
result.value:=a;
end;

class operator TSizeMeter.Implicit(a: TSizeMeter): single;
begin
result:=a.value;
end;

class operator TSizeMiliMeter.Add(a, b: TSizeMiliMeter): TSizeMiliMeter;
begin
result.value:=a.value+b.value;
end;

class operator TSizeMiliMeter.Subtract(a, b: TSizeMiliMeter): TSizeMiliMeter;
begin
result.value:=a.value-b.value;
end;

class operator TSizeMiliMeter.Implicit(a: single): TSizeMiliMeter;
begin
result.value:=a;
end;

class operator TSizeMiliMeter.Implicit(a: TSizeMiliMeter): single;
begin
result:=a.value;
end;

class operator TSizeMiliMeter.Implicit(a: TSizeMiliMeter): TSizeMeter;
begin
result.value:=a.value/1000;
end;

class operator TSizeMiliMeter.Implicit(a: TSizeMeter): TSizeMiliMeter;
begin
result.value:=a.value*1000;
end;

А ось його використання:

var v1:TSizeMeter;
v2:TSizeMiliMeter;
v3:TSizeMeter;
v4:TSizeMiliMeter;
begin
v1:=1.1;
v2:=111.1;
s1:=v1;
s2:=v2;
writeln(formatfloat('0.000',v1.value)+' '+v1.units+' or '+formatfloat('0.000',s1));
writeln(formatfloat('0.000',v2.value)+' '+v2.units+' or '+formatfloat('0.000',s2));
writeln('+');
v3:=v1+v2;
v4:=v1+v2;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',v4.value)+' '+v4.units);
writeln('-');
v3:=v1-v2;
v4:=v1-v2;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',v4.value)+' '+v4.units);
writeln('cast');
v3:=v2;
v4:=v1;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',v4.value)+' '+v4.units);
writeln('mix');
v3:=v2+22.22;
s1:=v1+33.33;
writeln(formatfloat('0.000',v3.value)+' '+v3.units);
writeln(formatfloat('0.000',s1));
end.

Що дасть ось такий результат:
1,100 m or 1,100
111,100 mm or 111,100
+
1,211 m
1211,100 mm

0,989 m
988,900 mm
cast
0,111 m
1100,000 mm
mix
0,133 m
34,430
Це рішення не ідеально, так як робить конвертацію неочевидною, що може породити нові проблеми. Але, якщо обьявлять всі змінні з коректним типом, проблем бути не повинно.
Більш жестое рішення, яке обмежить кастинг типів, це райзить эксепшн при спробі каста, типу так:

class operator TSizeMiliMeter.Implicit(a: TSizeMiliMeter): TSizeMeter;
begin
raise Exception.Create('Typecast not allowed');
end;

class operator TSizeMiliMeter.Implicit(a: TSizeMeter): TSizeMiliMeter;
begin
raise Exception.Create('Typecast not allowed');
end;

В цьому випадку ми отримаємо помилку в рядку:

v3:=v1+v2;

Можна розвинути рішення далі, створивши свій тип эксепшна.

Якщо хтось стикався з подібними проблемами, діліться досвідом в коментарях :) Напевно є більш елегантні рішення, ніж описане вище.

P. S: Тест виконувався в Delphi 10.1
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.