Mike Torrettinni 198 Posted August 22, 2020 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
Mike Torrettinni 198 Posted August 22, 2020 24 minutes ago, Anders Melander said: No. Oh, too bad. Thanks. Share this post Link to post
David Schwartz 426 Posted August 23, 2020 (edited) 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 August 23, 2020 by David Schwartz Share this post Link to post
Anders Melander 1783 Posted August 23, 2020 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
David Heffernan 2345 Posted August 23, 2020 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. 1 Share this post Link to post
Mike Torrettinni 198 Posted August 23, 2020 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
Mike Torrettinni 198 Posted August 23, 2020 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
David Heffernan 2345 Posted August 23, 2020 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
Mike Torrettinni 198 Posted August 23, 2020 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
Lars Fosdal 1792 Posted August 23, 2020 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
David Heffernan 2345 Posted August 23, 2020 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
Mike Torrettinni 198 Posted August 23, 2020 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
Mike Torrettinni 198 Posted August 23, 2020 (edited) 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 August 23, 2020 by Mike Torrettinni Share this post Link to post
Lars Fosdal 1792 Posted August 23, 2020 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; 1 Share this post Link to post
Mike Torrettinni 198 Posted August 23, 2020 Aha, I see you wrap everything into record helper, consts and FromString function. I didn't really know about it. Share this post Link to post
Anders Melander 1783 Posted August 23, 2020 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 Share this post Link to post
Lars Fosdal 1792 Posted August 23, 2020 3 minutes ago, Anders Melander said: Hard coded translations... Wow 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
Mike Torrettinni 198 Posted August 23, 2020 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
Anders Melander 1783 Posted August 23, 2020 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
Lars Fosdal 1792 Posted August 23, 2020 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
Lars Fosdal 1792 Posted August 23, 2020 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
Mike Torrettinni 198 Posted August 23, 2020 (edited) 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 August 23, 2020 by Mike Torrettinni Share this post Link to post
David Heffernan 2345 Posted August 23, 2020 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
Mike Torrettinni 198 Posted August 23, 2020 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