7

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?

Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327
  • `class operator TWeatherCode.Implicit(const Value: String): TWeatherCode;` - this code is wrong, and you Delphi cries you a warning about that. What would be the result when `Value = 'D111'` ? What would be the result when `Value = 'xABC' ` ? – Arioch 'The Dec 30 '16 at 01:59
  • @Arioch'The My IDE doesn't produce any hints or warnings while building. And I have yet to add this protection, it's still in progress :-) – Jerry Dodge Dec 30 '16 at 02:01
  • It should - you CASE has no safety checks against not-listed elements... You better do put all the protection FROM START - it never works adding it later, you just simply FORGET some of the places. So basically you start with skeleton of "it is error!!!" checks, and then later you add correct non-error values. That way you may forget some correct value - which you would find in your checks, but you would not forget error-protection – Arioch 'The Dec 30 '16 at 02:06
  • @Arioch'The I was in the middle of that actually, I'm getting ready to finish it and I'll update my code above. Usually I do start around errors. You caught me in the moment I had not yet checked. Not sure why I don't get any warnings like you do, I strive to resolve all of them. I have a package with 12 units and zero hints or warnings on a full build. – Jerry Dodge Dec 30 '16 at 02:08
  • `if Value[3] in ['0'..'4'] then` - that is what is called "magic constants". It is very unfriendly towards potential future type extensions - you would not be able to scan ALL your sources for those range-checkings and reliably identify and update them - there would be way too many 0 digits, way too many 4 digits, etc. You have to use `Low` and `High` functions instead - see my code for one of possible ways. – Arioch 'The Dec 30 '16 at 02:28
  • @Arioch'The Indeed, it's still in the works. I do intend to change these soon. I'm just taking higher priority matters first. Weak error checking is better than no error checking :-) In the meantime, I'm trying to fix the rest of my system to stay within Weather Underground's ridiculously low limits on their free account :-/ – Jerry Dodge Dec 30 '16 at 02:30
  • "there is nothing so permanent as temporary solutions". even if you would have spare time to add ( better ) error checking post-factum - you no more would be able to remember ALL the places that need it. Been there... – Arioch 'The Dec 30 '16 at 02:35
  • @Arioch'The Way I see it, weather standards haven't changed in years, or even decades. In such an event, I would have to update this record type anyway to accommodate for the description string. I take your experienced advise 100% - just in some rare particular cases like this, things won't change any time soon. And if they do, I'll be all on top of it. Call me lazy, sure. Just I tackle code in order of importance, and come back and fill in the less important parts later. You should see all the `//TODO` comments in the rest of my code :P – Jerry Dodge Dec 30 '16 at 02:41
  • you would have to update record type, and you are to make damn sure, Delphi would NOT BE ABLE TO COMPILE your code, if you update the record type and forget to update some subroutines. See, that is what compilation and strong typing is for, you have to write your sources the way that compiler would choke of most of potentially possible errors. "Die early" principle. The earlier the error is checked - the easier it gets. And this compiler should be told to catch any error possible. I think you never had to maintain 15 years old code, full of *undocumented* "this can never happens" assumptions – Arioch 'The Dec 30 '16 at 02:46
  • http://stackoverflow.com/q/876089/976391 – Arioch 'The Dec 30 '16 at 02:46
  • @Arioch'The Lol actually I work with a 22 year old software system full of that sort of thing. I guess that's why I've gotten so lazy. – Jerry Dodge Dec 30 '16 at 02:48
  • Well.... one day I was called to find a bug. The local variable was changing value. It turned out that happened during a call to some unrelated function. No one looked at it because - guess what? - "that can never happen, don't waste my time looking there". After I pressed the guy (20 years older than me) into doing what I want to be done to catch a bug in HIS code, we found that "impossible happens, right here". It turned out that function called function that called function that..... 8 calls below there was an *undocumented assumption* that `char` is `byte` – Arioch 'The Dec 30 '16 at 02:52
  • 1
    ....and that long-tested always-working function after changing Delphi version started landing covert precision strikes at local variables of a functions 8 calls up the stack. So, do you really want to be the guy hiding those landmines for future unlucky ones? While walking in their shoes here and now? Have some sympathy if nothing else :-D – Arioch 'The Dec 30 '16 at 02:54
  • 1
    This is not a cast. This is encoding. Why do you want to encode? What will you do with the encoded value? – David Heffernan Dec 30 '16 at 09:24

2 Answers2

5

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.

Arioch 'The
  • 15,799
  • 35
  • 62
  • o0o, as soon as I get back to my IDE - wasn't expecting something so quickly :D But what about `Cardinal` > `TWeatherCode`? – Jerry Dodge Dec 30 '16 at 01:54
  • absolutely the same trick: putting packed-array variable over the cardinal variable. Then the same conversion code just goes backwards – Arioch 'The Dec 30 '16 at 02:36
  • BTW, this Cardinal representation now is almost matching string conversion of yours, save but fixed size and non-Unicode characters. You may even base your to-string conversion out of it :-D Kind of `var T: packed arr........ute Var-Cardinal; begin ..... Result := StringOfChar(#0,4); Result[1] := T[1];...` – Arioch 'The Dec 30 '16 at 02:39
  • If I may ask, why use `Assert` if a developer may choose to turn assertions off? I would think raising a manual exception is the appropriate handling of unsupported values. I could extend into my own `EInvalidWeatherCode` class. – Jerry Dodge Dec 30 '16 at 04:30
  • Well, I suppose developer after any significant change would at least ONCE run the program with debug mode. Just... `Assert` is kind of self-documenting. It is shouting out my intention "i fear bad case and here I check for it". And actually i'd prefer compile-time exception, I think I just made it wrong. Which.... Yeah, I guess I am not still moved completely from D5 to DXE2 :-D – Arioch 'The Dec 30 '16 at 08:05
3

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.

EugeneK
  • 2,164
  • 1
  • 16
  • 21
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/132029/discussion-on-answer-by-eugenek-how-to-cast-a-records-attributes-to-integer-and). – Martijn Pieters Jan 01 '17 at 20:16