I have written the following record type with implicit operators to cast between this record type and a string. It represents a standard weather code which briefly describes current weather conditions:
type
TDayNight = (dnDay, dnNight);
TCloudCode = (ccClear = 0, ccAlmostClear = 1, ccHalfCloudy = 2, ccBroken = 3,
ccOvercast = 4, ccThinClouds = 5, ccFog = 6);
TPrecipCode = (pcNone = 0, pcSlight = 1, pcShowers = 2, pcPrecip = 3, pcThunder = 4);
TPrecipTypeCode = (ptRain = 0, ptSleet = 1, ptSnow = 2);
TWeatherCode = record
public
DayNight: TDayNight;
Clouds: TCloudCode;
Precip: TPrecipCode;
PrecipType: TPrecipTypeCode;
class operator Implicit(const Value: TWeatherCode): String;
class operator Implicit(const Value: String): TWeatherCode;
function Description: String;
function DayNightStr: String;
end;
implementation
{ TWeatherCode }
class operator TWeatherCode.Implicit(const Value: TWeatherCode): String;
begin
case Value.DayNight of
dnDay: Result:= 'd';
dnNight: Result:= 'n';
end;
Result:= Result + IntToStr(Integer(Value.Clouds));
Result:= Result + IntToStr(Integer(Value.Precip));
Result:= Result + IntToStr(Integer(Value.PrecipType));
end;
class operator TWeatherCode.Implicit(const Value: String): TWeatherCode;
begin
if Length(Value) <> 4 then raise Exception.Create('Value must be 4 characters.');
case Value[1] of
'd','D': Result.DayNight:= TDayNight.dnDay;
'n','N': Result.DayNight:= TDayNight.dnNight;
else raise Exception.Create('First value must be either d, D, n, or N.');
end;
if Value[2] in ['0'..'6'] then
Result.Clouds:= TCloudCode(StrToIntDef(Value[2], 0))
else
raise Exception.Create('Second value must be between 0 and 6.');
if Value[3] in ['0'..'4'] then
Result.Precip:= TPrecipCode(StrToIntDef(Value[3], 0))
else
raise Exception.Create('Third value must be between 0 and 4.');
if Value[4] in ['0'..'2'] then
Result.PrecipType:= TPrecipTypeCode(StrToIntDef(Value[4], 0))
else
raise Exception.Create('Fourth value must be between 0 and 2.');
end;
function TWeatherCode.DayNightStr: String;
begin
case DayNight of
dnDay: Result:= 'Day';
dnNight: Result:= 'Night';
end;
end;
function TWeatherCode.Description: String;
begin
case Clouds of
ccClear: Result:= 'Clear';
ccAlmostClear: Result:= 'Mostly Clear';
ccHalfCloudy: Result:= 'Partly Cloudy';
ccBroken: Result:= 'Cloudy';
ccOvercast: Result:= 'Overcast';
ccThinClouds: Result:= 'Thin High Clouds';
ccFog: Result:= 'Fog';
end;
case PrecipType of
ptRain: begin
case Precip of
pcNone: Result:= Result + '';
pcSlight: Result:= Result + ' with Light Rain';
pcShowers: Result:= Result + ' with Rain Showers';
pcPrecip: Result:= Result + ' with Rain';
pcThunder: Result:= Result + ' with Rain and Thunderstorms';
end;
end;
ptSleet: begin
case Precip of
pcNone: Result:= Result + '';
pcSlight: Result:= Result + ' with Light Sleet';
pcShowers: Result:= Result + ' with Sleet Showers';
pcPrecip: Result:= Result + ' with Sleet';
pcThunder: Result:= Result + ' with Sleet and Thunderstorms';
end;
end;
ptSnow: begin
case Precip of
pcNone: Result:= Result + '';
pcSlight: Result:= Result + ' with Light Snow';
pcShowers: Result:= Result + ' with Snow Showers';
pcPrecip: Result:= Result + ' with Snow';
pcThunder: Result:= Result + ' with Snow and Thunderstorms';
end;
end;
end;
end;
Examples of strings that can be cast to and from this type are...
d310
= Cloudy and Light Rain (Day)d440
= Overcast with Rain and Thunderstorms (Day)n100
= Mostly Clear (Night)This string will always be in this format, and will always be 4 characters: 1 letter and 3 numbers. In reality, you can look at it as the following options:
What I would like to do is also provide an option to implicitly cast it to an integer, or even byte, if I can get it small enough. I would hate however to have to add a ton of if/then/else or case statements.
I know that it is possible to take a given (small) set of characters and cast them to a single value. However, I have no idea how it's done. I do know, for example, that this technique is used in places such as the flags on DrawTextEx
and other similar WinAPI calls. I think this may relate to the usage of shr
/ shl
but I have no idea how to use those, and am terrible at that type of math.
How can I cast these 4 attributes combined into a single integer or byte, and cast them back?
The simplest thing would be to pack it into 32-bit signed or unsigned integer.
I'd prefer unsigned one (aka Cardinal
)
You say - "will always be 4 characters: 1 letter and 3 numbers".
And by "character" you mean a lower Latin character.
Those characters are well represented by single-byte AnsiChar
type.
Warning: in different Delphi versions Char
is a shortcut to either WideChar
or AnsiChar
types. When doing lo-level types conversion you're to avoid ambiguous higher level shortcuts and use raw types.
Same goes for string
-> UnicodeString
or AnsiString
ambiguity.
class operator TWeatherCode.Implicit(const Value: TWeatherCode): Cardinal;
var R: packed array [1..4] of AnsiChar absolute Result;
begin
Assert( SizeOf( R ) = SizeOf ( Result ) );
// safety check - if we ( or future Delphi versions ) screwed data sizes
// or data alignment in memory - it should "go out with a bang" rather
// than corrupting your data covertly
case Value.DayNight of
dnDay: R[1] := 'd';
dnNight: R[1] := 'n';
else raise ERangeError.Create('DayNight time is incorrect!');
// future extensibility safety check needed.
// Compiler is right with Warnings here
end;
R[2] := AnsiChar(Ord('0') + Ord(Value.Clouds));
R[3] := AnsiChar(Ord('0') + Ord(Value.Precip));
R[4] := AnsiChar(Ord('0') + Ord(Value.PrecipType));
end;
Similar thing to go back.
class operator TWeatherCode.Implicit(const Value: Cardinal): TWeatherCode;
var V: packed array [1..4] of AnsiChar absolute Value;
B: array [2..4] of Byte;
begin
Assert( SizeOf( V ) = SizeOf ( Value ) );
// safety check - if we ( or future Delphi versions ) screwed data sizes
// or data alignment in memory - it should "go out with a bang" rather
// than corrupting your data covertly
case UpCase(V[1]) of
'D': Value.DayNight:= TDayNight.dnDay;
'N': Value.DayNight:= TDayNight.dnNight;
else raise .....
end;
B[2] := Ord(V[2]) - Ord('0');
B[3]....
B[4]....
// again: error-checking is going first, before actual processing legit values. that makes it harder to forget ;-)
if (B[2] < Low(Value.Clouds)) or (B[2] > High(Value.Clouds)) then
raise ERangeError(ClassName + ' unpacking from Cardinal, #2 element is: ' + V[2] );
Value.Clouds := TCloudCode( B[2] );
.......
end;
UPD. Good question about assertions below. I hope in more or less modern Delphi that assertion should be rewritten into compile-time check. I am not totally sure of syntax, I almost never used it, but it should be something like that:
// Assert( SizeOf( R ) = SizeOf ( Result ) );
{$IF SizeOf( R ) <> SizeOf ( Result )}
{$MESSAGE FATAL 'Data size mismatch, byte-tossing operations are broken!'}
{$IFEND}
Now back to packing into one byte.... Simple bits-jockey, slicing your byte into four independent parts, it would not do. See - https://en.wikipedia.org/wiki/Power_of_two
Your components are requiring the following cells:
1+3+3+2 = 9 > 8 bits = 1 byte.
Still you can try to get along with variable-base https://en.wikipedia.org/wiki/Positional_notation
Total number of combinations is 2*7*5*3 = 210 combinations. Less than 256 in one byte.
So you may get away with something like that:
Result :=
Ord(Value.DayNight) + 2*(
Ord(Value.Clouds) + 7*(
Ord(Value.Precip) + 5*(
Ord(Value.PrecipType) + 3*(
0 {no more 'digits' }
))));
This would do the trick, but i'd be very wary about it. The more you pack the data - the less redundancy it has. The less redundancy - the more chance that a random erratic value would look LIKE some legit data. Also that gives you no room for future extension,
Imagine in future the #3 element would be extended from 0, 1, 2, 3, 4
range to 0 to 5
range. You would have to change the formula. But... would you really know to do it? Or would your old program be fed with new data? And if you do change the formula - how would you be able to tell bytes calculated with new and old formulas???
Frankly, I did similar tricks to pack dates into two bytes. But I had 100% warranty I would never extend those dates, and extra I have 100% means to tell written date from non-initialized space, and - yet another extra - in case of extension required, I knew I would be able to drop that obsoleted scheme it completely and start afresh in another memory location. No compatibility with older data would be ever required. I am not so sure about your case.
You can declare your record with variant part as following
TWeatherCode = record
public
class operator Implicit(const Value: TWeatherCode): String;
class operator Implicit(const Value: String): TWeatherCode;
class operator Implicit(const Value: TWeatherCode): Cardinal;
class operator Implicit(const Value: Cardinal): TWeatherCode;
function Description: String;
function DayNightStr: String;
public
case Storage: Boolean of
True: (
DayNight: TDayNight;
Clouds: TCloudCode;
Precip: TPrecipCode;
PrecipType: TPrecipTypeCode);
False: (IntValue: Cardinal);
end;
...
class operator TWeatherCode.Implicit(const Value: TWeatherCode): Cardinal;
begin
Result := Value.IntValue;
end;
class operator TWeatherCode.Implicit(const Value: Cardinal): TWeatherCode;
begin
Result.IntValue := Value;
end;
in this case you can either assign individual fields or IntValue, they will use the same memory.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With