Jump to content

Recommended Posts

I have this example of TProjectType and the consts of values and record helper.

 

type
  TProjectType = (ptMain, prExternal);
const
  cProjectTypeXMLNames: array[TProjectType] of string = ('xml_main', 'xml_external'); // Project type imported from XML
  cProjectTypeNames:    array[TProjectType] of string = ('Main project', 'External project');  // Project name to show user

type
  TProjectTypeHelper = record helper for TProjectType
    function ToString: string;
    function XMLName: string;
  end;

function TProjectTypeHelper.ToString : string;
begin
  Result := cProjectTypeNames[Self];
end;

function TProjectTypeHelper.XMLName : string;
begin
  Result := cProjectTypeXMLNames[Self];
end;

function GetProjectTypeFromXMLName(const aName: string): TProjectType;
var
  i: TProjectType;
begin
  Result := TProjectType(0); // default
  for i := Low(TProjectType) to High(TProjectType) do
  if cProjectTypeXMLNames[i] = aName then
    Exit(i);
end;

 

If I add 10 more enums, I need to copy 10x all this code, and make changes as needed, but template stays the same. For TDocumentType I would just copy&paste all, set new enum and consts values and replace ProjectType -> DocumentType in helper and function.

 

Is there a way to use generics, so I don't need to copy&paste this for other enums?

Share this post


Link to post

Ahh, mapping between enums and strings. Who needs to do THAT? 

 

Sadly, what you're pointing at is a very common problem that php solves reasonably well, but not Pascal nor even C or C++. I don't know about C#.

 

The syntactical requirements to define a structure (record or class) then initialize it are such that it can't be done all in the same place. The closest you can get in Delphi is butt-ugly and convoluted.

 

In php, if you stuff a bunch of things into a variable, the language infers that it's an array, and the syntax isn't much different if it's a list.

 

And you can define data in a structured way that lets you stuff it into a variable that just "knows" it's structured data. Maybe from type inferencing, but php is an interpreted language, so it's a lot more flexible than compiled languages that need you to define structures to receive data, and then separately assign values to that data even if it's just static initializers that run when the program starts up.

 

Anyway, helpers were created as a way of extending records and classes that you cannot otherwise touch. Using helpers to "extend" enums does not make much sense. I can see WHY you'd want to do it, but that's not what helpers were designed for.

 

Honestly, this is such a common thing (in my world, anyway) that I don't see why they haven't added something to the language to support it. It was nice for about 5 minutes when they added the ability to get the string representation of an enum name, but you don't want to show users things like "ptMain" or "ptExternal" -- you want something more relatable. But you can only give them a numeric value; you should be able to assign a string that can be accessed for "people" to read. You end up forced to set up a completely separate data structure for that, and then initializing it requires a third chunk of code. Or you need a case statement that maps the enums to strings, which means if you add more stuff to the enum, the compiler won't tell you that you forgot to extend your mappings to include alternate descriptions for them.

 

To make matters worse, since case statements in Delphi don't accept strings as discriminants (like most other modern-day languages) you cannot simply invert the case statement for mapping from the string back to the enum. In fact, you need a totally different mechanism. So you'll end up with either two TDictionaries (one to map each way) or one that lets you index off of any value in the tuples. Or a case to map one way plus a TDictionary to go the other way. A butt-ugly solution for such a common problem, IMHO.

 

You can use TStringLists with their ability to manage <name=value> pairs, but they're strings. <enum=string> isn't supported.

Edited by David Schwartz

Share this post


Link to post
7 hours ago, David Schwartz said:

Sadly, what you're pointing at is a very common problem that php solves reasonably well, but not Pascal nor even C or C++. I don't know about C#.

 

The syntactical requirements to define a structure (record or class) then initialize it are such that it can't be done all in the same place. The closest you can get in Delphi is butt-ugly and convoluted.

So you're complaining about stuff like this?:

type
  TFooBarKind = (fbFoo, fbBar);
  
  TFooBar = record
    This: integer;
    That: string;
  end;

const
  FooBars = array[TFooBarKind] of TFooBar = (
    (This: 1; That: "Foo"),
    (This: 2; That: "Bar")
  );

Apart from the little detail that PHP doesn't have enums at all the solutions are pretty much the same for Delphi, PHP, C, C++ and C# as far as I can see...

Are you maybe confusing compile-time with run-time?

Share this post


Link to post

I do this;

  [Names(
    'Curvature',
    'Bend angle'
  )]
  TPreBendSpecifiedBy = (
    pbsCurvature,
    pbsBendAngle
  );

and then I can write:

Assert(Enum.Name(pbsCurvature) = 'Curvature'))

Of course this requires some library code behind it, but it's pretty convenient.

  • Thanks 1

Share this post


Link to post
19 minutes ago, David Heffernan said:

I do this;


  [Names(
    'Curvature',
    'Bend angle'
  )]
  TPreBendSpecifiedBy = (
    pbsCurvature,
    pbsBendAngle
  );

and then I can write:


Assert(Enum.Name(pbsCurvature) = 'Curvature'))

Of course this requires some library code behind it, but it's pretty convenient.

If I have 10 different types, would I still need to defined 10 different Attribute classes:

type ProjectAttribute = class(TCustomAttribute)
...

Using Attributes, could I avoid setting same code for 10 different types?

 

 

Share this post


Link to post
9 hours ago, David Schwartz said:

You can use TStringLists with their ability to manage <name=value> pairs, but they're strings. <enum=string> isn't supported.

Yes, probably not really usable for my case of multiple values connected to enum.

 

I wish this function would be somehow shortened or connected to type, like record helpers. Now it kind of stands on its own.

function GetProjectTypeFromXMLName(const aName: string): TProjectType;

Share this post


Link to post
18 minutes ago, Mike Torrettinni said:

If I have 10 different types, would I still need to defined 10 different Attribute classes:

As you can see from the code, the attribute class is called NamesAttribute and is independent from the type. Otherwise the whole thing would be pointless. 

Share this post


Link to post
1 minute ago, David Heffernan said:

As you can see from the code, the attribute class is called NamesAttribute and is independent from the type. Otherwise the whole thing would be pointless. 

Yes, you are right. For a few minutes I was excited about the possibility 🙂

Share this post


Link to post

It is tricky to do a generic helper class for Enumerations, since there is no constraint that will allow you to use enum operators.

There are workarounds, but not pretty ones.


As for having to use a constant table - why not use the actual type name?
 

There is System.TypInfo function to convert an enum to a string which reflects the actual type name.

http://docwiki.embarcadero.com/Libraries/Sydney/en/System.TypInfo.GetEnumName

Result :=  GetEnumName(TypeInfo(TProjectType), Ord(Self));

 

There also is the reverse

http://docwiki.embarcadero.com/Libraries/Sydney/en/System.TypInfo.GetEnumValue

 

Share this post


Link to post
10 minutes ago, Mike Torrettinni said:

Yes, you are right. For a few minutes I was excited about the possibility 🙂

You have misunderstood me. The attribute is independent of the type. Therefore it can be used with any type. 

Share this post


Link to post
1 minute ago, David Heffernan said:

You have misunderstood me. The attribute is independent of the type. Therefore it can be used with any type. 

Aha, but if each type requires different attribute values, each type has to have it's own attribute. That's how I understand your example and documentation, but have no actual experience with attributes, yet.

Share this post


Link to post
9 minutes ago, Lars Fosdal said:

It is tricky to do a generic helper class for Enumerations, since there is no constraint that will allow you to use enum operators.

There are workarounds, but not pretty ones.


As for having to use a constant table - why not use the actual type name?
 

There is System.TypInfo function to convert an enum to a string which reflects the actual type name.

http://docwiki.embarcadero.com/Libraries/Sydney/en/System.TypInfo.GetEnumName

Result :=  GetEnumName(TypeInfo(TProjectType), Ord(Self));

 

There also is the reverse

http://docwiki.embarcadero.com/Libraries/Sydney/en/System.TypInfo.GetEnumValue

 

Yes, I do use that in some cases, but rarely when the value is shown to user - there will always be 1 value that is not 'presentable'. (no spaces between words, not all xml values comply with standards...)

Edited by Mike Torrettinni

Share this post


Link to post

It probably is no consolation that I also have hundreds of these helpers...

  TAllowState = (asAllow,asWarn,asDeny);
  TAllowStateHelper = record helper for TAllowState
  const
    Translate: array[TAllowState] of xlt = (
     { asAllow } (no:'Tillatt';       se:'Tillåten';      en:'Allow'),
     { asWarn }  (no:'Vis advarsel';  se:'Visa varning';  en:'Show warning'),
     { asDeny }  (no:'Sperret';       se:'Sperrad';       en:'Denied')
    );
  public
    function ToString(const Language: TLanguage = langDefault): String; inline;
    class function FromString(str: string; const Language: TLanguage = langDefault): TAllowState; static;
  end;

{ TAllowStateHelper }

class function TAllowStateHelper.FromString(str: string; const Language: TLanguage): TAllowState;
begin
  Result:=asWarn; //Default
  var Lang := Language.GetLanguage;
  for var state :=Low(TAllowState) to High(TAllowState)
   do if CompareText(str, state.ToString(Lang)) = 0
    then Exit(state);
end;

function TAllowStateHelper.ToString(const Language: TLanguage): String;
begin
  Result := Translate[Self].Text(Language);
end;

 

  • Thanks 1

Share this post


Link to post
5 minutes ago, Lars Fosdal said:

Translate: array[TAllowState] of xlt = ( { asAllow } (no:'Tillatt'; se:'Tillåten'; en:'Allow'), { asWarn } (no:'Vis advarsel'; se:'Visa varning'; en:'Show warning'), { asDeny } (no:'Sperret'; se:'Sperrad'; en:'Denied') );

Hard coded translations... Wow :classic_huh:

Share this post


Link to post
3 minutes ago, Anders Melander said:

Hard coded translations... Wow :classic_huh:

Normally we use Sisulizer, but it is hard to keep track of context in Sisulizer, so for enums we have this "dirty" workaround which also prevents missing translations.

 

Share this post


Link to post
Just now, Lars Fosdal said:

Normally we use Sisulizer, but it is hard to keep track of context in Sisulizer, so for enums we have this "dirty" workaround which also prevents missing translations.

 

I assume you have some code-gen tool for any changes to these enums, or adding new types? Excel? 🙂

Share this post


Link to post
Just now, Lars Fosdal said:

which also prevents missing translations

Sure, but I mean.. I know it sucks to use arrays of resourcestrings but this must be hell to maintain. And no spell check or automation of any kind.

 

Share this post


Link to post

 

Just now, Mike Torrettinni said:

I assume you have some code-gen tool for any changes to these enums, or adding new types? Excel? 🙂

Not really. Usually I just create a keyboard macro on the fly to make the template, and then I fill in the texts afterwards.

Share this post


Link to post
Just now, Anders Melander said:

Sure, but I mean.. I know it sucks to use arrays of resourcestrings but this must be hell to maintain. And no spell check or automation of any kind.

 

Maintain, how?  Once the text is written, it rarely needs to be changed after any initial correction.
 

Sentences for UI texts are definitively not ideal - but this worked out so well that we even use this method for hip computer voice responses in multiple languages for our JsonRPC services.  We still haven't introduced a fourth language, but if we do - it will be impossible to miss a translation.

Share this post


Link to post
1 hour ago, David Heffernan said:

I do this;


  [Names(
    'Curvature',
    'Bend angle'
  )]
  TPreBendSpecifiedBy = (
    pbsCurvature,
    pbsBendAngle
  );

and then I can write:


Assert(Enum.Name(pbsCurvature) = 'Curvature'))

Of course this requires some library code behind it, but it's pretty convenient.

@David Heffernan Would you still use Attributes if you have 2 sets of string values (Names, XMLNames...) for a type?

 

This doesn't compile:

  [Names('Curvature', 'Bend angle' )]
  [XMLNames('XMLCurvature', 'XMLBendAngle' )]
  TPreBendSpecifiedBy = ( pbsCurvature, pbsBendAngle
  );

 

Edited by Mike Torrettinni

Share this post


Link to post
1 hour ago, Mike Torrettinni said:

Aha, but if each type requires different attribute values, each type has to have it's own attribute. That's how I understand your example and documentation, but have no actual experience with attributes, yet.

Unless all of your enumerated types have the same names, it seems like you'd have to provide names for each type. 

Share this post


Link to post
2 hours ago, David Heffernan said:

Unless all of your enumerated types have the same names, it seems like you'd have to provide names for each type. 

That's good. So, when I have type and values 1:1, I can use Attributes and no need for helpers.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×