<emphasis>Getting Things Done</emphasis> <emphasis role="bold">Scanning Landing Zone Files</emphasis> Here’s the scenario—you’ve just received a data file from someone and it has been put on your landing zone. Before you spray that file to your Thor cluster and start to work with it, you want to have a quick look to see exactly what kind of data it contains and whether the format of that data matches the format that you were given by the supplier. There are a number of ways to do this, including mapping a drive to your landing zone and using a text/hex editor to open the file and look at the contents. This article will show you how to accomplish this from within QueryBuilder using ECL. Here’s the code (contained in the Default.ProgGuide MODULE attribute): EXPORT MAC_ScanFile(IP, infile, scansize) := MACRO ds := DATASET(FileServices.ExternalLogicalFileName(IP, infile), {DATA1 S}, THOR )[1..scansize]; OUTPUT(TABLE(ds,{hex := ds.s,txt := (STRING1)ds.s}),ALL); Rec := RECORD UNSIGNED2 C; DATA S {MAXLENGTH(8*1024)}; END; Rec XF1(ds L,INTEGER C) := TRANSFORM SELF.C := C; SELF.S := L.s; END; ds2 := PROJECT(ds,XF1(LEFT,COUNTER)); Rec XF2(Rec L,Rec R) := TRANSFORM SELF.S := L.S[1 .. R.C-1] + R.S[1]; SELF := L; END; Rolled := ROLLUP(ds2,TRUE,XF2(LEFT,RIGHT)); OUTPUT(TRANSFER(Rolled[1].S,STRING)); ENDMACRO; This is written as a MACRO because you could have multiple Landing Zones, and you certainly are going to want to look into different files each time. Therefore, a MACRO that generates the standard process code to scan the file is precisely what’s needed here. This MACRO takes three parameters: the IP of the landing zone containing the file the fully-qualified path to that file on the landing zone the number of bytes to read (maximum 8K) The initial DATASET declaration uses the FileServices.ExternalLogicalFileName function to name the file. Defining the RECORD structure as a single DATA1 field is necessary to ensure that both text and binary fields can be read correctly. Specifying the DATASET as a THOR file (no matter what type of file it actually is) makes it simple to read as a fixed-length record file. The square brackets at the end of the DATASET declaration automatically limit the number of 1-byte records read to the first scansize number of bytes in the file. The first OUTPUT action allows you to see the raw Hexadecimal data from the file. The TABLE function doubles up the input data, producing a DATA1 displaying the Hex value and a STRING1 that type casts each byte to a STRING1 for display. Viewing the raw Hex value is necessary because most binary fields will not contain text-displayable characters (and those that do may mislead you as to the actual contents of the field). Non-displayable binary characters show up as a square box in the text column display. Next, we’ll construct a more text-friendly view of the data. To do that we’ll start with the Rec RECORD structure, which defines a byte-counter field (UNSIGNED2 C) and a variable-length field (DATA S {MAXLENGTH(8*1024)} to contain the text representation of the data as a single horizontal line of text. The XF1 TRANSFORM and its associated PROJECT moves the data from the input format into the format needed to roll up that data into a single text string. Adding the byte-counter field is necessary to ensure that blank spaces are not accidentally trimmed out of the final display. The XF2 TRANSFORM and its associated ROLLUP function performs the actual data append. The TRUE condition parameter ensures that only one record will result containing all the input bytes rolled into a single record. The last OUTPUT action uses the TRANSFER function instead of type casting to ensure that all the text characters in the original data are accurately represented. You call this MACRO like this: ProgGuide.MAC_ScanFile( '10.173.9.4', 'C:\\training\\import\\BOCA.XML', 200) When viewing the result, the QueryBuilder Result_1 tab displays a column of hexadecimal values and the text character (if any) next to it in the second column. This byte-by-byte view of the data is designed to allow you to see the raw Hexadecimal values of each byte alongside its text representation. This is the primary view to use when looking at the contents of files containing binary data. The QueryBuilder Result_2 tab displays a single record with a single field. You can click on that field to highlight it, right-click and select “Copy” from the popup menu, then paste the text into any text editor to view. Binary fields will appear as square blocks or “garbage” characters, depending on their hex value. Once pasted into a text editor, you can easily look for data patterns that indicate the start for fields or records and validate that the data layout information provided by the data vendor is accurate (or not). <emphasis role="bold">Cartesian Product of Two Datasets</emphasis> A Cartesian Product is the product of two non-empty sets in terms of ordered pairs. As an example, if we take the set of values, A, B and C, and a second set of values, 1, 2, and 3, the Cartesian Product of these two sets would be the set of ordered pairs, A1, A2, A3, B1, B2, B3, C1, C2, C3. The ECL code to produce this kind of result from any two input datasets would look like this (contained in Cartesian.ECL): OutFile1 := '~PROGGUIDE::OUT::CP1'; rec := RECORD STRING1 Letter; END; Inds1 := DATASET([{'A'},{'B'},{'C'},{'D'},{'E'}, {'F'},{'G'},{'H'},{'I'},{'J'}, {'K'},{'L'},{'M'},{'N'},{'O'}, {'P'},{'Q'},{'R'},{'S'},{'T'}, {'U'},{'V'},{'W'},{'X'},{'Y'}], rec); Inds2 := DATASET([{'A'},{'B'},{'C'},{'D'},{'E'}, {'F'},{'G'},{'H'},{'I'},{'J'}, {'K'},{'L'},{'M'},{'N'},{'O'}, {'P'},{'Q'},{'R'},{'S'},{'T'}, {'U'},{'V'},{'W'},{'X'},{'Y'}], rec); CntInDS2 := COUNT(Inds2); SetInDS2 := SET(inds2,letter); outrec := RECORD STRING1 LeftLetter; STRING1 RightLetter; END; outrec CartProd(rec L, INTEGER C) := TRANSFORM SELF.LeftLetter := L.Letter; SELF.RightLetter := SetInDS2[C]; END; //Run the small datasets CP1 := NORMALIZE(Inds1,CntInDS2,CartProd(LEFT,COUNTER)); OUTPUT(CP1,,OutFile1,OVERWRITE); The core structure of this code is the NORMALIZE that will produce the Cartesian Product. The two input datasets each have twenty-five records, so the number of result records will be six hundred twenty-five (twenty-five squared). Each record in the LEFT input dataset to the NORMALIZE will execute the TRANSFORM once for each entry in the SET of values. Making the values a SET is the key to allowing NORMALIZE to perform this operation, otherwise you would need to do a JOIN where the join condition is the keyword TRUE to accomplish this task. However, in testing this with sizable datasets (as in the next instance of this code below), the NORMALIZE version was about 25% faster than using JOIN. If there is more than one field, then multiple SETs may be defined and the process stays the same. This next example does the same operation as above, but first generates two sizeable datasets to work with (also contained in Cartesian.ECL): InFile1 := '~PROGGUIDE::IN::CP1'; InFile2 := '~PROGGUIDE::IN::CP2'; OutFile2 := '~PROGGUIDE::OUT::CP2'; //generate data files rec BuildFile(rec L, INTEGER C) := TRANSFORM SELF.Letter := Inds2[C].Letter; END; GenCP1 := NORMALIZE(InDS1,CntInDS2,BuildFile(LEFT,COUNTER)); GenCP2 := NORMALIZE(GenCP1,CntInDS2,BuildFile(LEFT,COUNTER)); GenCP3 := NORMALIZE(GenCP2,CntInDS2,BuildFile(LEFT,COUNTER)); Out1 := OUTPUT(DISTRIBUTE(GenCP3,RANDOM()),,InFile1,OVERWRITE); Out2 := OUTPUT(DISTRIBUTE(GenCP2,RANDOM()),,InFile2,OVERWRITE); // Use the generated datasets in a cartesian join: ds1 := DATASET(InFile1,rec,thor); ds2 := DATASET(InFile2,rec,thor); CntDS2 := COUNT(ds2); SetDS2 := SET(ds2,letter); CP2 := NORMALIZE(ds1,CntDS2,CartProd(LEFT,COUNTER)); Out3 := OUTPUT(CP2,,OutFile2,OVERWRITE); SEQUENTIAL(Out1,Out2,Out3) Using NORMALIZE in this case to generate the datasets is the same type of usage previously described in the Creating Example Data article. After that, the process to achieve the Cartesian Product is exactly the same as the previous example. Here’s an example of how this same operation can be done using JOIN (also contained in Cartesian.ECL): // outrec joinEm(rec L, rec R) := TRANSFORM // SELF.LeftLetter := L.Letter; // SELF.RightLetter := R.Letter; // END; // ds4 := JOIN(ds1, ds2, TRUE, joinEM(LEFT, RIGHT), ALL); // OUTPUT(ds4); <emphasis role="bold">Records Containing Any of a Set of Words</emphasis> Part of the data cleanup problem is the possible presence of profanity or cartoon character names in the data. This can become an issue whenever you are working with data that originated from direct input by end-users to a website. The following code (contained in the BadWordSearch.ECL file) will detect the presence of any of a set of “bad” words in a given field: SetBadWords := ['JUNK', 'GARBAGE', 'CRAP']; BadWordDS := DATASET(SetBadWords,{STRING10 word}); SearchDS := DATASET([{1,'FRED','FLINTSTONE'}, {2,'GEORGE','JETSON'}, {3,'CRAPOLA','NASTYGUY'}, {4,'JUNKER','JUNKEE'}, {5,'GARBAGEGUY','JUNKMAN'}, {6,'FREDDY','KRUEGER'}, {7,'TIM','JONES'}, {8,'JOHN','SMITH'}, {9,'MIKE','MALARKEY'}, {10,'GEORGE','KRUEGER'} ],{UNSIGNED6 ID,STRING10 firstname,STRING10 lastname}); outrec := RECORD SearchDS.ID; SearchDS.firstname; BOOLEAN FoundWord; END; {BOOLEAN Found} FindWord(BadWordDS L, STRING10 inword) := TRANSFORM SELF.Found := StringLib.StringFind(inword,TRIM(L.word),1) > 0; END; outrec CheckWords(SearchDS L) := TRANSFORM SELF.FoundWord := EXISTS(PROJECT(BadWordDS, FindWord(LEFT,L.firstname))(Found=TRUE)); SELF := L; END; result := PROJECT(SearchDS,CheckWords(LEFT)); OUTPUT(result(FoundWord=TRUE),NAMED('BadWordsInFirstName')); OUTPUT(result(FoundWord=FALSE),NAMED('NoBadWordsInFirstName')); This code is a simple PROJECT of each record that you want to search. The result will be a record set containing the record ID field, the firstname search field, and a BOOLEAN FoundWord flag field indicating whether any “bad” word was found. The search itself is done by a nested PROJECT of the field to be searched against the DATASET of “bad” words. Using the EXISTS function to detect if any records are returned from that PROJECT where the returned Found field is TRUE sets the FoundWord flag field value. The StringLib.StringFind function simoply detects the presence anywhere within the search strin of any of the “bad” words. The OUTPUT of the set of records where the FoundWord is TRUE allows post-processing to evaluate whether the record is worth keeping or garbage (probably requiring human intervention). The above code is a specific example of this technique, but it would be much more useful to have a MACRO that accomplishes this task, something like this one (also contained in the BadWordSearch.ECL file): MAC_FindBadWords(BadWordSet,InFile,IDfld,SeekFld,ResAttr,MatchType=1) := MACRO #UNIQUENAME(BadWordDS) %BadWordDS% := DATASET(BadWordSet,{STRING word{MAXLENGTH(50)}}); #UNIQUENAME(outrec) %outrec% := RECORD InFile.IDfld; InFile.SeekFld; BOOLEAN FoundWord := FALSE; UNSIGNED2 FoundPos := 0; END; #UNIQUENAME(ChkTbl) %ChkTbl% := TABLE(InFile,%outrec%); #UNIQUENAME(FindWord) {BOOLEAN Found,UNSIGNED2 FoundPos} %FindWord%(%BadWordDS% L, INTEGER C, STRING inword) := TRANSFORM #IF(MatchType=1) //"contains" search SELF.Found := StringLib.StringFind(inword,TRIM(L.word),1) > 0; #END #IF(MatchType=2) //"exact match" search SELF.Found := inword = L.word; #END #IF(MatchType=3) //"starts with" search SELF.Found := StringLib.StringFind(inword,TRIM(L.word),1) = 1; #END SELF.FoundPos := IF(SELF.FOUND=TRUE,C,0); END; #UNIQUENAME(CheckWords) %outrec% %CheckWords%(%ChkTbl% L) := TRANSFORM WordDS := PROJECT(%BadWordDS%,%FindWord%(LEFT,COUNTER,L.SeekFld)); SELF.FoundWord := EXISTS(WordDS(Found=TRUE)); SELF.FoundPos := WordDS(Found=TRUE)[1].FoundPos; SELF := L; END; ResAttr := PROJECT(%ChkTbl%,%CheckWords%(LEFT)); ENDMACRO; This MACRO does a bit more than the previous example. It begins by passing in: * The set of words to find* The file to search* The unique identifier field for the search record* The field to search in* The attribute name of the resulting recordset* The type of matching to do (defaulting to 1) Passing in the set of words to seek allows the MACRO to operate against any given set of strings. Specifying the result attribute name allows easy post-processing of the data. Where this MACRO starts going beyond the previous example is in the MatchType parameter, which allows the MACRO to use the Template Language #IF function to generate three different kinds of searches from the same codebase: a “contains” search (the default), an exact match, and a “starts with” search. It also has an expanded output RECORD structure that includes a FoundPos field to contain the pointer to the first entry in the passed in set that matched. This allows post processing to detect positional matches within the set so that “matched pairs” of words can be detected, as in this example (also contained in the BadWordSearch.ECL file): SetCartoonFirstNames := ['GEORGE','FRED', 'FREDDY']; SetCartoonLastNames := ['JETSON','FLINTSTONE','KRUEGER']; MAC_FindBadWords(SetCartoonFirstNames,SearchDS,ID,firstname,Res1,2) MAC_FindBadWords(SetCartoonLastNames,SearchDS,ID,lastname,Res2,2) Cartoons := JOIN(Res1(FoundWord=TRUE), Res2(FoundWord=TRUE), LEFT.ID=RIGHT.ID AND LEFT.FoundPos=RIGHT.FoundPos); MAC_FindBadWords(SetBadWords,SearchDS,ID,firstname,Res3,3) MAC_FindBadWords(SetBadWords,SearchDS,ID,lastname,Res4) SetBadGuys := SET(Cartoons,ID) + SET(Res3(FoundWord=TRUE),ID) + SET(Res4(FoundWord=TRUE),ID); GoodGuys := SearchDS(ID NOT IN SetBadGuys); BadGuys := SearchDS(ID IN SetBadGuys); OUTPUT(BadGuys,NAMED('BadGuys')); OUTPUT(GoodGuys,NAMED('GoodGuys')); Notice that the position of the cartoon character names in their separate sets define a single character name to search for in multiple passes. Calling the MACRO twice, searching for the first and last names separately, allows you to post-process their results with a simple inner JOIN where the same record was found in each and, most importantly, the positional values of the matches are the same. This prevents “GEORGE KRUEGER” from being mis-labelled a cartoon chracter name. <emphasis role="bold">Simple Random Samples</emphasis> There is a statistical concept called a “Simple Random Sample” in which a statistically “random” (different from simply using the RANDOM() function) sample of records is generated from any dataset. The algorithm inmplemented in the following code example was provided by a customer. This code is implemented as a MACRO to allow multiple samples to be produced in the same workunit (contained in the SimpleRandomSamples.ECL file): SimpleRandomSample(InFile,UID_Field,SampleSize,Result) := MACRO //build a table of the UIDs #UNIQUENAME(Layout_Plus_RecID) %Layout_Plus_RecID% := RECORD UNSIGNED8 RecID := 0; InFile.UID_Field; END; #UNIQUENAME(InTbl) %InTbl% := TABLE(InFile,%Layout_Plus_RecID%); //then assign unique record IDs to the table entries #UNIQUENAME(IDRecs) %Layout_Plus_RecID% %IDRecs%(%Layout_Plus_RecID% L, INTEGER C) := TRANSFORM SELF.RecID := C; SELF := L; END; #UNIQUENAME(UID_Recs) %UID_Recs% := PROJECT(%InTbl%,%IDRecs%(LEFT,COUNTER)); //discover the number of records #UNIQUENAME(WholeSet) %WholeSet% := COUNT(InFile) : GLOBAL; //then generate the unique record IDs to include in the sample #UNIQUENAME(BlankSet) %BlankSet% := DATASET([{0}],{UNSIGNED8 seq}); #UNIQUENAME(SelectEm) TYPEOF(%BlankSet%) %SelectEm%(%BlankSet% L, INTEGER c) := TRANSFORM SELF.seq := ROUNDUP(%WholeSet% * (((RANDOM()%100000)+1)/100000)); END; #UNIQUENAME(selected) %selected% := NORMALIZE( %BlankSet%, SampleSize, %SelectEm%(LEFT, COUNTER)); //then filter the original dataset by the selected UIDs #UNIQUENAME(SetSelectedRecs) %SetSelectedRecs% := SET(%UID_Recs%(RecID IN SET(%selected%,seq)), UID_Field); result := infile(UID_Field IN %SetSelectedRecs% ); ENDMACRO; This MACRO takes four parameters: * The name of the file to sample * The name of the unique identifier field in that file * The size of the sample to generate * The name of the attribute for the result, so that it may be post-processed The algorithm itself is fairly simple. We first create a TABLE of uniquely numbered unique identifier fields. Then we use NORMALIZE to produce a recordset of the candidate records. Which candidate is chosen each time the TRANSFORM function is called is determined by generating a “random” value between zero and one, using modulus division by one hundred thousand on the return from the RANDOM() function, then multiplying that result by the number of records to sample from, rounding up to the next larger integer. This determines the position of the field identifier to use. Once the set of positions within the TABLE is determined, they are used to define the SET of unique fields to use in the final result. This algorithm is designed to produce a sample “with replacement” so that it is possible to have a smaller number of records returned than the sample size requested. To produce exactly the size sample you need (that is, a “without replacement” sample), you can request a larger sample size (say, 10% larger) then use the CHOOSEN function to retrieve only the actual number of records required, as in this example (also contained in the SimpleRandomSamples.ECL file). SomeFile := DATASET([{'A1'},{'B1'},{'C1'},{'D1'},{'E1'}, {'F1'},{'G1'},{'H1'},{'I1'},{'J1'}, {'K1'},{'L1'},{'M1'},{'N1'},{'O1'}, {'P1'},{'Q1'},{'R1'},{'S1'},{'T1'}, {'U1'},{'V1'},{'W1'},{'X1'},{'Y1'}, {'A2'},{'B2'},{'C2'},{'D2'},{'E2'}, {'F2'},{'G2'},{'H2'},{'I2'},{'J2'}, {'K2'},{'L2'},{'M2'},{'N2'},{'O2'}, {'P2'},{'Q2'},{'R2'},{'S2'},{'T2'}, {'U2'},{'V2'},{'W2'},{'X2'},{'Y2'}, {'A3'},{'B3'},{'C3'},{'D3'},{'E3'}, {'F3'},{'G3'},{'H3'},{'I3'},{'J3'}, {'K3'},{'L3'},{'M3'},{'N3'},{'O3'}, {'P3'},{'Q3'},{'R3'},{'S3'},{'T3'}, {'U3'},{'V3'},{'W3'},{'X3'},{'Y3'}, {'A4'},{'B4'},{'C4'},{'D4'},{'E4'}, {'F4'},{'G4'},{'H4'},{'I4'},{'J4'}, {'K4'},{'L4'},{'M4'},{'N4'},{'O4'}, {'P4'},{'Q4'},{'R4'},{'S4'},{'T4'}, {'U4'},{'V4'},{'W4'},{'X4'},{'Y4'} ],{STRING2 Letter}); ds := DISTRIBUTE(SomeFile,HASH(letter[2])); SimpleRandomSample(ds,Letter,6, res1) //ask for 6 SimpleRandomSample(ds,Letter,6, res2) SimpleRandomSample(ds,Letter,6, res3) OUTPUT(CHOOSEN(res1,5)); //actually need 5 OUTPUT(CHOOSEN(res3,5)); <emphasis role="bold">Hex String to Decimal String</emphasis> An email request came to me to suggest a way to convert a string containing Hexadecimal values to a string containing the decimal equivalent of that value. The problem was that this code needed to run in Roxie and the StringLib.String2Data plugin library fiunction was not available for use in Roxie queries at that time. Therefore, an all-ECL solution was needed. This example function (contained in the Hex2Decimal.ECL file) provides that functionality, while at the same time demonstrating practical usage of BIG ENDIAN integers and type transfer. HexStr2Decimal(STRING HexIn) := FUNCTION //type re-definitions to make code more readable below BE1 := BIG_ENDIAN UNSIGNED1; BE2 := BIG_ENDIAN UNSIGNED2; BE3 := BIG_ENDIAN UNSIGNED3; BE4 := BIG_ENDIAN UNSIGNED4; BE5 := BIG_ENDIAN UNSIGNED5; BE6 := BIG_ENDIAN UNSIGNED6; BE7 := BIG_ENDIAN UNSIGNED7; BE8 := BIG_ENDIAN UNSIGNED8; TrimHex := TRIM(HexIn,ALL); HexLen := LENGTH(TrimHex); UseHex := IF(HexLen % 2 = 1,'0','') + TrimHex; //a sub-function to translate two hex chars to a packed hex format STRING1 Str2Data(STRING2 Hex) := FUNCTION UNSIGNED1 N1 := CASE( Hex[1], '0'=>00x,'1'=>10x,'2'=>20x,'3'=>30x, '4'=>40x,'5'=>50x,'6'=>60x,'7'=>70x, '8'=>80x,'9'=>90x,'A'=>0A0x,'B'=>0B0x, 'C'=>0C0x,'D'=>0D0x,'E'=>0E0x,'F'=>0F0x,00x); UNSIGNED1 N2 := CASE( Hex[2], '0'=>00x,'1'=>01x,'2'=>02x,'3'=>03x, '4'=>04x,'5'=>05x,'6'=>06x,'7'=>07x, '8'=>08x,'9'=>09x,'A'=>0Ax,'B'=>0Bx, 'C'=>0Cx,'D'=>0Dx,'E'=>0Ex,'F'=>0Fx,00x); RETURN (>STRING1<)(N1 | N2); END; UseHexLen := LENGTH(TRIM(UseHex)); InHex2 := Str2Data(UseHex[1..2]); InHex4 := InHex2 + Str2Data(UseHex[3..4]); InHex6 := InHex4 + Str2Data(UseHex[5..6]); InHex8 := InHex6 + Str2Data(UseHex[7..8]); InHex10 := InHex8 + Str2Data(UseHex[9..10]);; InHex12 := InHex10 + Str2Data(UseHex[11..12]); InHex14 := InHex12 + Str2Data(UseHex[13..14]); InHex16 := InHex14 + Str2Data(UseHex[15..16]); RETURN CASE(UseHexLen, 2 => (STRING)(>BE1<)InHex2, 4 => (STRING)(>BE2<)InHex4, 6 => (STRING)(>BE3<)InHex6, 8 => (STRING)(>BE4<)InHex8, 10 => (STRING)(>BE5<)InHex10, 12 => (STRING)(>BE6<)InHex12, 14 => (STRING)(>BE7<)InHex14, 16 => (STRING)(>BE8<)InHex16, 'ERROR'); END; This HexStr2Decimal FUNCTION takes a variable-length STRING parameter containing the hexadecimal value to evaluate. It begins by re-defining the eight possible sizes of unsigned BIG ENDIAN integers. This re-definition is purely for cosmetic purposes—to make the subsequent code more readable. The next three attributes detect whether an even or odd number of hexadecimal characters have been passed. If an odd number is passed, then a “0” character is prepended to the passed value to ensure the hex values are placed inthe correct nibbles. The Str2Data FUNCTION takes a two-character STRING parameter and translates each character into the appropriate hexadecimal value for each nibble of the resulting 1-character STRING that it returns. The first character defines the first nibble and the second defines the second. These two values are ORed together (using the bitwise | operator) then the result is type transferred to a one-character string, using the shorthand syntax— (>STRING1<) —so that the bit pattern remains untouched. The RETURN result from this FUNCTION is a STRING1 because each succeeding two-character portion of the HexStr2Decimal FUNCTION’s input parameter will pass through the Str2Data FUNCTION and be concatenated with all the preceding results. The UseHexLen attribute determines the appropriate size of BIG ENDIAN integer to use to translate the hex into decimal, while the InHex2 through InHex16 attributes define the final packed-hexadecimal value to evaluate. The CASE function then uses that UseHexLen to determine which InHex attribute to use for the number of bytes of hex value passed in. Only even numbers of hex characters are allowed (meaning the call to the function would need to add a leading zero to any odd-numbered hex values to translate) and the maximum number of characters allowed is sixteen (representing an eight-byte packed hexadecimal value to translate). In all cases, the result from the InHex attribute is type-transferred to the appropriately sized BIG ENDIAN integer. The standard type cast to STRING then performs the actual value translation from the hexadecimal to decimal. The following calls return the indicated results: OUTPUT(HexStr2Decimal('0101')); // 257 OUTPUT(HexStr2Decimal('FF')); // 255 OUTPUT(HexStr2Decimal('FFFF')); // 65535 OUTPUT(HexStr2Decimal('FFFFFF')); // 16777215 OUTPUT(HexStr2Decimal('FFFFFFFF')); // 4294967295 OUTPUT(HexStr2Decimal('FFFFFFFFFF')); // 1099511627775 OUTPUT(HexStr2Decimal('FFFFFFFFFFFF')); // 281474976710655 OUTPUT(HexStr2Decimal('FFFFFFFFFFFFFF')); // 72057594037927935 OUTPUT(HexStr2Decimal('FFFFFFFFFFFFFFFF')); // 18446744073709551615 OUTPUT(HexStr2Decimal('FFFFFFFFFFFFFFFFFF')); // ERROR