Data Formats

Until now, our focus has been on how to interface with the HotSync manager, and how to use the Sync Suite API to synchronize databases and records. It was easy to read and write the actual record data, because we only supported ASCII strings. A real Palm program is going to have a much more complicated record structure, with a mixture of string and binary data.

The sample code for this section includes a new AppForge project, Ch4b.vbp., and a new conduit project, Ch4bCond.vbp. Together, these show how to handle the low-level chores associated with packing and unpacking record data, converting numbers between the Intel and Motorola formats, and handling differences between Palm and Windows date formats.

Note that if you use the AppForge database utilities and the Universal Conduit, you don’t have to worry about packing and unpacking records and fields, or converting between Palm PDA and Windows data types. We covered the database utilities in Chapter 3, and will look at the Universal Conduit in Chapter 5.

Creating Packed Record Data

Our new application creates a database that consists of structured records with four fields: a Date value, a Time value, a Boolean value, an Integer value, a Long integer, and a String with 20 characters. Figure 4-9 shows the application’s user interface on the Palm PDA.

Mains screen for sample application Ch4b

Figure 4-9. Mains screen for sample application Ch4b

First, let’s look at how to create a packed record on the Palm. You’ll find this code in the form module Ch4b.frm in the new application.

The AppForge documentation provides the size of each of the built-in data types: four bytes for a Date or Time field, one byte for a Boolean, two bytes for an Integer, four bytes for a Long, and one byte for each character in a string variable.

When the user presses the WriteDB button in the application, we use PDBCreateRecord to allocate a record large enough to hold the six fields.

Dim Count As Long
Count = 4 + 4 + 1 + 2 + 4 + 20
PDBCreateRecord hPdb, Count

We pack the fields into our record in this order: Date, Time, Boolean, Integer, Long, and then String. Knowing the layout of the fields—their type, order and size—is critical when unpacking the data. AppForge lists the sizes of the basic data types in their documentation. As under Windows, both Date and Time values are stored in the Date data type.

The PDBWriteFieldByOffset stores the binary representation of a value into the record. Here’s how to write a single Long integer field into the Palm record:

Dim lVar As Long
lVar = CLng(AFTextLong.Text)
PDBWriteFieldByOffset hPdb, 5, lVar

Note that this function requires that the field offset into the record be specified. In this example, we are writing a Long value at offset 5. The AppForge VB runtime engine already knows that the size of this variable is 4 bytes, so we don’t have to pass that also.

After we have written all six fields, we call PDBUpdateRecord to flush the new record into the Palm database.

Reading Packed Record Data

Now let’s look at how to unpack the field data in the conduit. There are two main issues we address when unpacking the record data: finding the start and end of fields, and converting formats between the Palm device and Windows. You’ll find the code for this section in Ch4bLogic.cls—note the ‘b'—in the new conduit. This conduit is structured just like the first one.

Locating the field data is just the reverse of how the record was created. First we get the record bytes into the variant array Data, using one of the PDRecordAdapter Read iterator functions. Because the array data is a byte-wise copy of the Palm record, the offsets used for writing are identical to those used for reading.

If the data is byte- or character-oriented, such as String or Boolean data, simply copy the needed bytes from the array:

Dim b As Boolean
b = Data(8)

For String data, you need to know the size ahead of time. In this example, we read from offset 15 until the end of the record data:

Dim s As String
pdUtil.ByteArrayToBSTR Data, 15, UBound(Data) - LBound(Data) + 1 - 15, s

It is probably overkill to use the UBound and LBound functions here. These are two of the rare VB functions that index from zero, not one, which is why there is an extra +1 in the ByteArrayToBSTR function call.

Numeric data, such as Integer or Long values, must be converted from Motorola to Intel byte ordering. The PDUtility object has methods that explicitly do this: SwapWORD and SwapDWORD take 16-bit and 32-bit quantities and reverse the high and low bytes as appropriate.

There is another issue when converting numbers: the data in the array is byte-oriented, while we want whole Integer or Long values. Here’s how we convert both at once in the sample conduit:

Dim i As Integer
pdUtil.ByteArrayToWORD Data, 9, True, i

The ByteArrayToWORD function locates the two bytes at offset 9 in the data array and converts them to an Integer quantity, and it swaps the byte ordering as well. We request the swapping by passing True as the third parameter; if you don’t want the implicit conversion, pass False instead. To convert a 32-bit quantity, use the ByteArrayToDWORD function.

Converting Dates and Times

Dates and times are the hardest values to convert, because they are a blend of application code and operating system convention. Dates and times on the Palm are represented in the Palm operating system as seconds since January 1, 1904. How these values are stored in database files, however, varies wildly from application to application.

The Palm native applications, for example, use a packed unsigned 16-bit quantity to represent the date: 7 bits for the year since 1904, 4 bits for the month, and 5 bits for the day. These applications use an unsigned 16-bit quantity to represent the time: 8 bits for the hour, and 8 bits for the minute.[32]

Our application stores dates and times exactly as returned by the AppForge VB runtime functions Date and Time. These values are the Palm operating system values: seconds since January 1, 1904. Depending on how the value is constructed, a Date value may or may not have the day or time component:

Dim d As Date
d = Date        ' No time component
d = Date + Time ' Has time component

Although it’s not documented, AppForge Date values are 32-bit quantities.

First we extract the 32-bit date from the packed record data using ByteArrayToDWORD (not shown). Then we call PalmLongToDate to convert the Long value to a VB Date value. The code for PalmLongToDate is shown in Example 4-14.

Example 4-14. Listing for PalmLongToDate

Private Function PalmLongToDate(ByVal d As Long) As Date

    Dim SecsSince1904 As Double
    Dim DaysSince1904 As Double

    Const SecsPerDay As Long = 86400
    Const UnsignedLngMax As Double = 4294967296#
    
    ' Handle signed/unsigned issues and use a double to prevent overflow.
    If d < 0 Then
        SecsSince1904 = UnsignedLngMax + d
    Else
        SecsSince1904 = d
    End If
    
    ' Figure out how many days have passed since 1904. Then add to
    ' earliest possible date. Let VB adjust for leap year, etc!
    DaysSince1904 = SecsSince1904 / SecsPerDay
    PalmLongToDate = DateSerial(1904, 1, 1) + CLng(DaysSince1904)
    
End Function

First we convert the Long argument, which represents seconds since January 1, 1904, into a double. We also adjust the argument if it is negative. This is necessary because the Palm data type is unsigned, so a negative number indicates that we have lost a bit! Adding the huge UnsignedLngMax brings it back.[33]

Next, we can calculate how many days have passed since January 1, 1904.

DaysSince1904 = SecsSince1904 / SecsPerDay

That’s the hard part. Finally, we create a VB Date with the initial magic value using the DateSerial function, and add the correct number of days to it to obtain the converted date:

PalmLongToDate = DateSerial(1904, 1, 1) + CLng(DaysSince1904)

By using VB date arithmetic, we avoid issues such as leap year, which are better handled by the operating system and runtime libraries.

Warning

PalmLongToDate is only accurate if it is given a pure date—one that has no time component. During normal integer division, the remainder is silently discarded. But because the calculation to get DaysSince1904 is done in Double arithmetic, this truncation doesn’t occur. This can cause the function to be inaccurate with some inputs.

Compared to getting the Date, the Time routine is almost trivial. It is shown in Example 4-15.

Example 4-15. Listing for PalmLongToTime

Private Function PalmLongToTime(ByVal T As Long) As Date

    Dim Hours As Integer
    Dim Minutes As Integer
    Dim Seconds As Integer
    
    ' Strip off any vestigal seconds, handle signed/unsigned issues.
    If T < 0 Then T = T + &H10000
    T = T And &H1FFFF
    
    ' Calculate hours, minutes and seconds based
    Hours = T \ 3600
    Minutes = (T \ 60) Mod 60
    Seconds = T Mod 60

    PalmLongToTime = TimeSerial(Hours, Minutes, Seconds)
    
End Function

The Time value is stored in the lower 17 bits of the 32-bit quantity. This means that we don’t have to worry about overflow while converting between signed and unsigned formats:

If T < 0 Then T = T + &H10000
T = T And &H1FFFF

We can discard any high-order bits that belong to a Date component, which means that PalmLongToTime may be safely called with any valid Date.

At this point, the variable T holds the number of seconds since midnight; this is converted to hours, minutes, and seconds. Adding these together with the VB TimeSerial function gives us the correct time, which we return as the value of the function.



[32] We don’t cover the native Palm date and time formats here. The AppForge knowledge base has an article with a code sample on how to convert those formats.

[33] Additionally, this is why we convert to a double internally. The conversion to unsigned 32-bit data would cause a silent overflow and our dates would be incorrect.

Get Programming Visual Basic for the Palm OS now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.