LAST PAGE  BACK TO INDEX  NEXT PAGE

[16.0] Programming Tricks (2)

[16.0] Programming Tricks (2)

v5.0 / 16 of 23 / 01 sep 99 / gvg

* More fun little gimmicks.


[16.1] CONVERTING A NUMBER TO A BINARY STRING
[16.2] PRINTING NUMBERS WITH LEADING ZEROES
[16.3] EURO NUMBER FORMATS
[16.4] DISPLAYING ENGINEERING FORMAT NUMBERS
[16.5] A JULIAN-DATE FUNCTION
[16.6] ADVANCED TRANSACTION I/O
[16.7] I/O DATA FORMATTING EXAMPLE
[16.8] MORE I/O DATA FORMATTING

 BACK TO INDEX

[16.1] CONVERTING A NUMBER TO A BINARY STRING

* For some odd reason (that nobody seems to recollect) you can use transaction objects in VEE to convert a number (say, 233) to an octal string ("351") or hex string ("0E9"), but you can't convert one to binary string format ("011101001" and so on).

So I devised a function of my own, called "BinFmt", to do this (it is part of the function library xgenfunc.vee). It has the overall layout:


   +-----------------------------------------------------------------------+
   |                                BinFmt                                 |
   +---+---------------------------------------------------------------+---+
   |   |       +------------------------------+                        |   |
   |   |       |          To String           |                        |   |
   |   |       +---+----------------------+---+      +--------+        |   |
   |   |   +-->| A | WRITE TEXT a OCT EOL | R +--+-->| strLen +--+     |   |
   |   |   |   +---+----------------------+---+  |   +--------+  |     |   |
   |   |   |                                     |               |     |   |
   |   |   |   +---------------------------------|---------------+     |   |
   |   |   |   |                                 |                     |   |
   |   |   |   |                  +--------------+                     |   |
   |   |   |   |                  |                                    |   |
   |   |   |   |   +-----------+  +-->+---------+   +-------------+    |   |
   | A +---+   +-->| For Count +----->| Formula +-->| Accumulator +--->| R |
   |   |           +-----------+  +-->+---------+   +-------------+    |   |
   |   |                          |                                    |   |
   |   |      +---------------+   |                                    |   |
   |   |      |     Text      |   |                                    |   |
   |   |      +---------------+   |                                    |   |
   |   |      | 0000: 000     |   |                                    |   |
   |   |      | 0000: 001     |   |                                    |   |
   |   |      | 0000: 010     |   |                                    |   |
   |   |      | 0000: 011     +---+                                    |   |
   |   |      | 0000: 100     |                                        |   |
   |   |      | 0000: 101     |                                        |   |
   |   |      | 0000: 110     |                                        |   |
   |   |      | 0000: 111     |                                        |   |
   |   |      +---------------+                                        |   |
   +---+---------------------------------------------------------------+---+
 
The Formula box is central to this function:

   +-------------------------------------+
   |               Formula               |
   +---+------------------------+--------+
   | A |                        |        |
   | B | C[StrFromLen(A, B, 1)] | Result |
   | C |                        |        |
   +---+------------------------+--------+
 
The basic philosophy here is that the input value is converted to an octal string, and then each digit ("0" through "7") is converted to its binary string equivalent ("000" through "111"). These triplets are then concatenated together and shot out the output pin.

In more detail:

This works efficiently, but some caution must be taken in handling its output. VEE tends to convert back and forth between strings and numbers in an invisible fashion, and your string "1011100" will most likely evaluate to the decimal value 1,011,100. Be careful.

 TOP OF PAGE

[16.2] PRINTING NUMBERS WITH LEADING ZEROES

* Similarly, one user wanted to print numbers with leading zeroes in VEE. I decided to implement an example program that allowed the user to output a number with a specified fractional-part field width, and a specified integral-part field width using zero-padding. For example, if the user wanted a fractional-part field width of 3 and an integral-part field-width of 6, and printed:


   51.4
 
-- he'd get:

   000051.400
 
If the number is negative, the "-" is regarded as part of the field width, meaning that:

   -51.4
 
-- is printed as:

   -00051.400
 
The following UserFunction (it's in the xgenfunc.vee file) did the job:

   +-------------------------------------------------------------------+
   |                       UserFunction: ZeroFmt                       |
   +------+--------------------------------------------------------+---+
   |      |                                                        |   |
   | Nval +--+                                                     |   |
   |      |  |                                                     |   |
   |      |  |   +------------------------------------------+      |   |
   |      |  |   |                 To String                |      |   |
   |      |  |   +---+----------------------------------+---+      |   |
   |      |  +-->| A | WRITE TEXT abs(A) REAL FIX:B EOL | r +--+   |   |
   | Fdig +--|-->| B |                                  |   |  |   |   |
   |      |  |   +---+----------------------------------+---+  |   |   |
   |      |  |                                                 |   |   |
   |      |  |                 +-------------------------------+   |   |
   |      |  |                 |                                   |   |
   |      |  +-----------------|-->+---------+                     |   |
   | Idig +--------------------|-->| Formula +-------------------->| R |
   |      |                    +-->+---------+                     |   |
   |      |                                                        |   |
   +------+--------------------------------------------------------+---+
 
All the "To String" box does is format the number to the specified number of fractional digits (it specifies a fixed output format with the number of fractional digits given by the "B" input pin. Note that the absolute value is taken of the input value since an output minus sign would screw things up). This formatted output string is then passed to the "Formula" box, which prefixes the string with the proper number of leading zeroes (and restores the minus sign if need be).

The "Formula" box is the tricky part; it has three inputs:

The formula it contains is as follows:


   (A<0 ? "-" : "") + strFromLen((B*"0"), 0, (B - strLen(intPart(A))))  + C
 
The following concepts are required to explain how it works:

This works cleanly. This particular formula does not blow up if the integral part is longer than the field width. I originally tried to do the fractional formatting in the "Formula" box, but this lead to some very peculiar fractional roundoff problems. I decided to use the right tool for the job and let the "To String" object do that for me.

Note that some care must be taken with the output, however. VEE handles data types interchangeably and some operations could convert this string back to a value, meaning the leading zeroes would be lost.

 TOP OF PAGE

[16.3] EURO NUMBER FORMATS

* In Europe, it is common to format numeric data in style that is reversed from that used in the US, with a comma between the integer and fractional part. Unfortunately there's no way to set this as a default in VEE, so I devised a UserFunction to do the job, with the syntax:


   ef(<nval>,<ndigits>)
 
-- where "nval" is a numeric value to be formatted and "ndigits" is 0 or more digits of precision to be displayed (the number is right-padded with zeroes if necessary).

The actual UserFunction (to be found in the xgenfunc.vee library) simply consists of a single To String object:


   +------------------------------------------------------+
   |                   UserFunction: ef                   |
   +---------+-----------------------------------+--------+
   |         |                                   |        |
   |         |      +------------------------+   |        |
   | nval    +--+   |        To String       |   |        |
   |         |  |   +---+--------------------+   |        |
   |         |  +-->| A |                    +-->| result |
   | ndigits +----->| B |                    |   |        |
   |         |      +---+--------------------+   |        |
   |         |                                   |        |
   +---------+-----------------------------------+--------+
 
The two transactions in the To String are a little devious, though:

   WRITE TEXT intpart(A), "," INT
   WRITE CASE sgn(abs(fracpart(A))) OF 
      B*"0", 
     (B==0 ? "" : intpart(fracpart(A)*10^B)) EOL
 
The first transaction:

   WRITE TEXT intpart(A), "," INT
 
-- just writes the integer part of the number (without an EOL). It's the second transaction (shown above as three lines for clarity, though it's actually just one) that's tricky.

The second transaction is based on a CASE statement because it has to ensure that the proper number of digits of precision are printed in any case. This might seem easy to do, just using a component of the third line above:


   intpart(fracpart(A)*10^B))
 
This takes the fractional part of the number, shifts it the required number of spaces by multiplying it by ten to the power of the required number of digits of precision, and then trims off the fractional part of that. However, this breaks down in two ways.

First, if the number of digits of precision is specified as "0", this still gives a trailing zero. So you have to test for that and just return a null string if that is the case:


   (B==0 ? "" : intpart(fracpart(A)*10^B))
 
Second, if there is no fractional part to the numeric value, this will always return a null string no matter what the number of digits of precision is, and we want to pad with zeroes. So we have to implement this as a CASE statement to perform two different actions, depending on whether the result is zero or not. This is done with:

   WRITE CASE sgn(abs(fracpart(A))) OF 
 
This takes the absolute value of the fractional part of the input number and then runs it through the signum function, which returns "0" for a zero value and "1" for a positive value. (It will return "-1" for a negative number, but we eliminated negative values by using the "abs()" function.) If the result is "0", the first case is evaluated:

   B*"0"
 
This multiplies the string "0" times the number of digits of precision to create a string of zeros as long as the number of digits of precision (this is a string multiplication, not a numeric multiplication). If the result is 1, the fractional part is given by:

   (B==0 ? "" : intpart(fracpart(A)*10^B))
 
This not only provides a useful function, but also insights in how to use the CASE transaction in conjuction with math functions to perform complicated control operations. However, as you can see, this leads to extremely tricky code and some documentation is essential if even you want to remember later how the thing works.

 TOP OF PAGE

[16.4] DISPLAYING ENGINEERING FORMAT NUMBERS

* While VEE has the capability to display engineering format numbers in Alphanumeric objects and the like, transaction objects cannot provide output in an engineering format. The only way around this is to create a UserFunction that provides such a format.

Such a UserFunction can be created using two objects:


   +-------------------------------------------------------+
   |                        EngFmt                         |
   +------+--------------------------------------------+---+
   |      |                                            |   |
   | NVal +---+-----------------+                      |   |
   |      |   |                 |      +----------+    |   |
   |      |   |   +---------+   +----->|          |    |   |
   |      |   +-->| Formula +--------->|    To    +--->| R |
   |      |       +---------+   +----->|  String  |    |   |
   |      |                     |  +-->|          |    |   |
   | Ndig +---------------------+  |   +----------+    |   |
   |      |                        |                   |   |
   | Meas +------------------------+                   |   |
   |      |                                            |   |
   +------+--------------------------------------------+---+
 
This UserFunction is passed the numeric value to be formatted ("Nval"), the number of digits after the decimal point to be displayed ("Ndig"), and the designation for the quantity being specified ("Meas"), such as "F" for "farads", "Hz" for hertz, and so on.

It is useful to specify this designation as a parameter to the function so that it can be returned as part of the result string -- otherwise, the result would simply be intepreted as a floating-point number, and VEE would reformat it as it pleased. You can specify a null string ("") as the parameter if you don't like this feature (or make it a "_" or some other harmless character if you don't want to mess with reformatting hassles).

The To String object in the UserFunction does most of the formatting. It uses the CASE transaction to select the proper metric suffix for the output. However, the CASE transaction requires a nice 0-1-2-3- kind of input to act as an index; the Formula box creates this index.

The mapping can be given by the following table:


   __________________________________________________________

   magnitude    metric_prefix   suffix_character  index_value
   __________________________________________________________

    e12          tera            "T"               10
    e9           giga            "G"                9
    e6           mega            "M"                8
    e3           kilo            "k"                7
    e0           -               ""                 6
    e-3          milli           "m"                5
    e-6          micro           "u"                4
    e-9          nano            "n"                3
    e-12         pico            "p"                2
    e-15         femto           "f"                1
    e-18         atto            "a"                0
   __________________________________________________________
 
The formula box creates this mapping with a formula as follows (displayed on multiple lines for clarity, though in practice it's all on the same line in the formula box):

   ( A==0 ? 6 : 
     ( A>=1e15 ? 10 :
       ( A<1e-18 ? 0 :
          intpart((log10(abs(A))+18)/3)
       )
     )
   )
 
The three conditional operators test for the zero value and values beyond the range of normal metrix prefixes, and assign the appropriate indexes for the suffix character. You have to nail the zero value in this way, because the core calculation takes a log value, and there are no logs for values of zero or less.

This done, the core calculation takes the base-ten log of the absolute value of the input (taking the absolute value ensures that it doesn't try to take a log of a negative number), which gives the decimal exponent needed in the range of -18 to 14. A bias value of 18 is then added to shift the values to a range of 0 to 32, and is then integer-divided by 3 to give the index from 0 to 10, as required.

Now this index value is fed to the To String object, along with the other parameters passed to the UserFunction:


  +-------------------------------------------------------------------------+
  |                                To String                                |
  +------+--------------------------------------------------------------+---+
  | Nval | WRITE TEXT Nval*10^(18-3*Idx) REAL FIX:Ndig                  |   |
  | Idx  | WRITE CASE Idx OF "a","f","p","n","u","m","","k","M","G","T" | R |
  | Ndig | WRITE TEXT Meas EOL                                          |   |
  | Meas |                                                              |   |
  +------+--------------------------------------------------------------+---+
 
The first transaction scales the numeric value so that the proper suffix can be added and still keep the output value in the proper range. It reverse-engineers the index provided by the Formula box to create a multiplier to shift the input value by the required scale. It also outputs the value with the requested number of digits after the decimal point. Note that the EOL is suppressed.

The second transaction simply provides the proper suffix as designated by the index given by the Formula box using the CASE transaction. Again, the EOL is suppressed.

The third transaction tacks the "Meas" value on the end, and completes the string output by issuing an EOL.

* You could then incorporate the UserFunction in a Formula box as follows:


   +--------------------------------------------+
   |                  Formula                   |
   +---+----------------------------------------+
   | A | "The frequency is " + EngFmt(A,0,"Hz") +-->
   +---+----------------------------------------+
 
-- or, more relevant to our original purpose, in a To String box:

   +---------------------------------------------------------+
   |                       To String                         |
   +-------+-------------------------------------------------+
   | Ntest | WRITE TEXT "Test number: ",Ntest                |
   | Nval  | WRITE TEXT " / Frequency: ",EngFmt(Nval,0,"Hz") |
   | C0    | WRITE TEXT " / Channels: ",C0,";",C1 EOL        |
   | C1    |                                                 |
   +-------+-------------------------------------------------+
 
The source for this UserFunction is available in the User Library xgenfunc.vee.

 TOP OF PAGE

[16.5] A JULIAN-DATE FUNCTION

* An HP SE needed to get a Julian-date function for VEE -- that is, a function that returned the current day of the year, with the value from 1 to 365. I figured that would be easy to implement. In itself it was, but figuring out some of the calendar issues was tricky.

The biggest problem was calculating leap years. Every year that is evenly divisible by 4 is a leap year, with the exception of turn-of-the-century years like 1700, 1800, or 1900, but with the exception to the exceptions that any turn-of-the-century year that is divisible by 400 (such as the upcoming year 2000) is a leap year.

Well, anyway, the function turns out to be dead simple (see the xgenfunc.vee library for the source):


   +------------------------------------------------------+
   |                       Formula                        |
   +---+--------------------------------------------------+
   | A | 1 + IntPart(( A - dmytodate(1,1,year(A))/86400)) |
   +---+--------------------------------------------------+
 
This subtracts the time value (in seconds) at the beginning of the first day of the year from the current time value, then divides that by the number of seconds in the day (60 * 60 * 24 = 86,400), takes the integer part of the result, and adds 1 to get the day of the year.

This UserFunction accepts the current value of "now()" and returns an integer date:


   Julian(now())
 
This is simple. Learning about calendars was a little sticky, but did you know that there are actually proposals to make the year 13 months of 28 days each (with various adjustments thrown in once or twice a year)? Interesting idea, but I won't hold my breath waiting to see if it will happen.

 TOP OF PAGE

[16.6] ADVANCED TRANSACTION I/O

* VEE transaction I/O has greater capabilities than most users give it credit for. For example, A user had a VEE application where he wanted to be able to select the following strings under program control:


   PAT PR,
   PAT PR,PG,
   PAT PR,PB,
   PAT PR,PG,PB,
 
I suggested that TO STRING might be able to do this effectively, and so I ended up writing the following example to show how (see xstrslc.vee for the source):

 +-----------+     +-----------------------------------------------------+
 |   Enum    |     |                       To String                     |
 +-----------+     +---+---------------------------------------------+---+
 | <*> Val1  |     |   | WRITE TEXT "PAT PR,"                        |   |
 | < > Val2  |     |   | WRITE CASE A OF "","PG,","PB,","PG,PB," EOL |   |
 | < > Val3  +--+->| A |                                             | R +--+
 | < > Val4  |  |  |   |                                             |   |  |
 +-----------+  |  |   |                                             |   |  |
                |  +---+---------------------------------------------+---+  |
                |                                                           |
                |                           +-------------------------------+
                |                           |
                |    +-------------+        |   +-------------+        
                +--->|     Val1    |        +-->|   PAT PR,   |
                     +-------------+            +-------------+
 
The Enum object is used to drive a WRITE CASE statement in the To String object. The two AlphaNumeric objects at the bottom are just handy window dressing and not an essential part of the program. Some important comments:

Note that if you wanted to directly send this string to an instrument you could do exactly the same thing in a Direct I/O object. If you wanted to do it to a file you could do it in a To File object. All transaction-I/O objects support roughly the same transactions (within the limits of the medium they are performing the transactions through).

 TOP OF PAGE

[16.7] I/O DATA FORMATTING EXAMPLE

* VEE's flexible data-handling tools can greatly assist instrument-control tasks. For a useful example, a new user was trying to use VEE to talk with a Tektronix instrument and having a lot of trouble. I looked over his task and was able to sort it out quickly. It made an excellent example of what VEE does well.

The Tek instrument was of the sort that apparently thought it was talking to a printer or a terminal. It returned output of the form:


   #1 Measurement Results
   12:00:00
      
   Chrom/Lum Gain Delay
   Composite
   Field = 2 Line = 14
   Average  32
   _____________________________________________________________________

   Chroma Gain       104.8 %
   Chroma Delay       -7.3 n sec
   _____________________________________________________________________
 
What the user wanted to get was the two data values at the bottom.

I put this data in a String constant in order to dummy up the user application, then pulled the data out using the program (see xextract.vee for the source):


    +-----------------+
    | String Constant +--+
    +-----------------+  |
                         |
   +---------------------+
   |
   |   +----------------------+   +-----------------------------+
   |   |       Formula        |   |         From String         |
   |   +---+---------+--------+   +------+------------------+---+   +-------+
   +-->| A |[ A[7]  ]| Result +-->| AStr | READ TEXT X REAL | X +-->| 104.8 |
   |   +---+---------+--------+   +------+------------------+---+   +-------+
   |
   |   +----------------------+   +-----------------------------+
   |   |       Formula        |   |         From String         |
   |   +---+---------+--------+   +------+------------------+---+   +-------+
   +-->| A |[ A[8]  ]| Result +-->| AStr | READ TEXT X REAL | X +-->|  -7.3 |
       +---+---------+--------+   +------+------------------+---+   +-------+
 
The program simply read the data into an array (note that blank lines are skipped normally in file or direct-I/O), extracted the proper lines from the array, then grabbed the real number out of each (the From String objects simply discarded anything that didn't look like a number). In practice the user could use Direct I/O to replace From File and not notice the difference.

 TOP OF PAGE

[16.8] MORE I/O DATA FORMATTING

* A user had a serial instrument, and like many such devices, it had a bizarre programming model. The instrument returned 57-character strings of ASCII text that represented binary status bytes.

Now this sounds OK, but the details were strange:

The user wanted to obtain a particular field out of the data and convert the bytes in it to a numeric value.

The details of the instrument made life unusually difficult. I gave it some thought and came up with some conclusions:

Since I didn't have the instrument, I decided to use From File to simulate the user's system. I wrote the following file (using a sample provided by the user) to provide data strings:



   420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
   420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
   420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
   420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
                                   ^
 
Note that this was a 5-line file, the first line was blank. This ensured that a CR-LF preceded each string (since a CR-LF is the default line terminator on a PC). The specific byte I was after is marked with the "^" in this illustration.

I came up with the following program to read the file:


  +-------+ loop ...
  | Until +---+
  | Break |   |
  +---+---+   | get 
      |       | byte     byte == 10?        yes
      |   +---+---+     +--------------+----->---+
      |   | From  +---->| If/Then/Else |         |
      |   | File  +--+  +--------------+--+      |
      |   +-------+  |                    |      |
      |              |                    |      |
      |          +---+---+            +---+---+  |
      |          | Break |            | Next  |  |
      |          +-------+            +-------+  |
      |           bail out             no, get   |
      |           if EOF               next byte |
      |                                          |
      |       +------------<---------------------+
      |       |
      |       | get      swap          convert          +--------------+
      |       | string   characters    from hex         | AlphaNumeric |
      |   +---+---+     +---------+   +-------------+   +--------------+
      |   | From  +---->| Formula +-->| From String +-->|      66      |
      |   | File  +--+  +----+----+   +-------------+   +--------------+
      |   +-------+  |       |
      |              |       |
      |          +---+---+   |
      |          | Break |   |
      |          +-------+   |
      |          bail out    |
      |          if EOF      |  the message box here announces that you have
      |                      |  obtained a frame and asks if you want to get
      |                      |  another one 
      |                 +----+----+    
      |                 | Message +-------------+
      |                 |   Box   +--+          |
      |                 +---------+  |          |
      |                              |          |
      |                          +---+---+  +---+---+
 +----+----+       no, bail out  | Break |  | Next  | yes, loop again 
 | Message |                     +-------+  +-------+
 |   Box   |    
 +---------+ display "Done!"
 
The details are as follows:

* The program operates in a loop, with an Until Break object driving a From File object. The From File object has the transaction:


   READ BINARY X BYTE
 
-- and also has an EOF pin set up as an output to detect end-of-file condition. In practice, the user would use a Direct I/O object and have a timeout pin brought out, but the program works very much the same in either case.

If an EOF occurs, the Break object is activated and the program exits the loop. If a byte is retrieved, it is sent to the If/Then/Else object, where it is compared to the value 10 (the ASCII value for a line-feed):


   A == 10 
 
If there is no match, the "Else" pin of the If/Then/Else object fires a Next object to put the program through the next iteration of the loop and fetch a new byte. If there is a match, the "Then" pin is fired to get the rest of the data frame.

The data frame is fetched by another From File object with the transaction:


   READ TEXT X STR MAXFW:1 ARRAY 55
 
This reads all 55 characters in the data frame individually into elements of a string array where each element is a 1-character string. It makes it easier to manipulate the characters this way. As with the first From File object, the EOF pin has been set up and drives a Break object to exit the loop if an EOF occurs.

The Formula box takes the needed ASCII characters and concatenates them in the proper order:


   A[33] + A[32]
 
The two-character hex string is then shoved through a From String object containing the transaction:

   READ TEXT X HEX
 
-- which converts it to the proper numeric value.

The Formula box fires a Message Box that simply asks if you want to continue or not. If you click on Cancel, a Break object is executed to bail out of the loop, if you click on OK, a Next object kicks off another loop to start looking for line-feeds again. Note that these objects simply simulate any actions you might care to perform on the string.

When the Until Break bails out of the loop, it fires another Message Box to announce the program is over. This simply simulates whatever actions you might care to take in the program after obtaining the frames.

Note that since Windows buffers serial data, you should have no worries about losing frames as long as you check for data every now and then.

* I have to conclude, however, that this particular device confirms my impression that many of the outfits building serial instruments have no concept of how to actually build a programmable instrument. Certainly much more effective means of transferring a status string are available -- for example, they could have used a 488.2-definite-length block to read the data as binary:


   #255<byte1><byte2> ... <byte55>
 
In this scheme the block starts with a "#", followed by a "2" to specify that two digits follow -- "55" -- that specify the number of bytes to be sent.

The other alternative would have been to have simply sent the entire thing as a set of decimal numbers, coded in ASCII and separated by commas (so-called "comma-separated variable" format):


   34,25,88,128, ... ,99<CR-LF>
 
This is the simplest means of reading array data and can be implemented in almost any appropriate I/O control language.

 TOP OF PAGE


 LAST PAGE  BACK TO INDEX  NEXT PAGE