Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cast a record's attributes to integer and back?

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:

  • 0, 1
  • 0, 1, 2, 3, 4, 5, 6
  • 0, 1, 2, 3, 4
  • 0, 1, 2

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?

like image 309
Jerry Dodge Avatar asked Dec 30 '16 01:12

Jerry Dodge


2 Answers

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. 0 to 1 => 0..1 => 1 bit
  2. 0 to 6 => 0..7 => 3 bits
  3. 0 to 4 => 0..7 => 3 bits
  4. 0 to 2 => 0..3 => 2 bits

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.

like image 182
Arioch 'The Avatar answered Sep 20 '22 00:09

Arioch 'The


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.

like image 28
EugeneK Avatar answered Sep 19 '22 00:09

EugeneK