Lexicographically Sortable Unique IDs and Cryptographic Random in Clarion
Using randomly generated unique IDs has many advantages over table-based auto-numbered fields.
The most common option is to use UUIDs/GUIDs, which are 128 bits long but commonly stored as a 36-byte string.
Another option available for Clarion programmers is to use StringTheory guids, which are 16-byte strings containing uppercase letters and numbers.
I see two disadvantages with random IDs:
- There is no way to know in what order the records were added, which may be useful in many situations
- They cause index fragmentation in SQL databases
A solution exists for UUIDs/GUIDs with UUIDv7, and there’s a Clarion implementation available in ClarionHub. However, I prefer StringTheory GUIDs since they’re more compact and easier to use in manual SQL queries.
I came up with the idea of using Clarion’s standard date and time to create a large number that represents the current date and time in hundredths of seconds since December 28, 1800, that is, a Clarion standard date/time (similar to Unix time), encode it using base 36 to get the first 8 characters of the ID, then fill the remaining characters with random base 36 digits. I’ve uploaded an implementation and sample project to GitHub (tested with C11).
To integrate into your application:
- Copy
MakeSGuid.inc
andMakeSGuid.clw
toaccessory\libsrc\win
or your app folder. - Add
INCLUDE('MakeSGuid.inc'),ONCE
to your global map
Some comments about the code:
!!! <summary>
!!! Creates a lexicographically sortable unique id with uppercase letters and digits. Example: 90DKZP0B6WYLIFYZ
!!! </summary>
!!! <param name="pLength">The length of the id. Must be between 8 and 32, default is 16</param>
!!! <param name="pDate">Optional, used only when creating an id for old data</param>
!!! <param name="pTime">Optional, used only when creating an id for old data</param>
MakeSGuid PROCEDURE(LONG pLength = 16,LONG pDate = 0,LONG pTime = 0)!,STRING
systemTime LIKE(_SYSTEMTIME),AUTO !System date/time
hundredthsPerDay GROUP;LONG(8640000);LONG;END !24*60*60*100 (one day in hundredths of a second)
dayInt64 LIKE(INT64),OVER(hundredthsPerDay) !As int64
base36Constant GROUP;LONG(36);LONG;END !Base 36
base36Int64 LIKE(INT64),OVER(base36Constant) !As int64
timestampInt64 LIKE(INT64),AUTO !Date/time in hundredths of a second since Dec 28, 1800
timeInt64 LIKE(INT64),AUTO !Time value
modResult LIKE(INT64),AUTO !Modulo result
result STRING(32),AUTO !Generated ID
position LONG,AUTO !String position
base36Digits STRING('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') !Base 36 encoding table
timestampDigits EQUATE(8) !Base 36 digits for date/time (valid until 2694)
randomBytes STRING(24),AUTO !Crypto random bytes
randomByteArray BYTE,DIM(SIZE(randomBytes)),OVER(randomBytes) !As byte array
CODE
IF pLength < timestampDigits THEN pLength = timestampDigits. !Minimum length check
IF pLength > SIZE(result) THEN pLength = SIZE(result).!Maximum length check
IF NOT pDate
GetLocalTime(systemTime) !Get system time
pDate = DATE(systemTime.wMonth,systemTime.wDay,systemTime.wYear) !Convert to Clarion date
pTime = systemTime.wHour * 360000 + systemTime.wMinute * 6000 + systemTime.wSecond * 100 + systemTime.wMilliseconds * .10 + 1 !Convert to hundredths of a second
END
i64Assign(timestampInt64,pDate) !timestampInt64 = pDate
i64Mult(timestampInt64,dayInt64,timestampInt64) !timestampInt64 *= day
i64Assign(timeInt64,pTime) ; i64Add(timestampInt64,timeInt64,timestampInt64) !timestampInt64 += pTime
LOOP position = timestampDigits TO 1 BY -1 !Convert to base 36 (reverse order)
i64Mod(timestampInt64,base36Int64,modResult) !modResult = timestampInt64 % 36
result[position] = base36Digits[ modResult.lo + 1 ] !Get encoded digit
i64Div(timestampInt64,base36Int64,timestampInt64) !timestampInt64 /= 36
END
IF pLength > timestampDigits
BCryptGenRandom(0,randomBytes,pLength - timestampDigits,BCRYPT_USE_SYSTEM_PREFERRED_RNG) !Get random bytes
LOOP position = timestampDigits + 1 TO pLength !Add random digits
result[position] = base36Digits[ ( randomByteArray[ position - timestampDigits ] % 36 ) + 1 ] !Convert to base 36 (slight bias acceptable)
END
END
RETURN result[1 : pLength] !Return clipped ID
On my laptop (modern i9) it creates one million GUIDs in 1.1 seconds. The initial version used DECIMAL
for large number operations. Switching to int64
increased speed by roughly 50x. Adding AUTO
to the local variables reduced the time in half.
Clarion 11 i64.inc
doesn’t include the prototype for i64Mod
, but I found it exists in ClaRUN.dll
. Copying the prototype from i64Div
worked. svapifnc.inc
doesn’t include GetLocalTime
, but the prototype for GetSystemTime
also worked. Both prototypes are in the module’s MAP
.
Initially, I interrupted the first loop using IF i64Is0(dt64)
and performed string operations to left-pad the ID with zeros. However, it turns out that not breaking the loop achieves the same result with less code and is likely faster.
For many applications, it may be enough to use a length of 12 (8 bytes for date/time and 4 bytes of random), but in my tests of creating one million queue records in a few seconds, there were some duplicates. Using the default length of 16 worked reliably.
Using MyId = MakeSGuid(8, MyDate, MyTime)
is a way to encode date and time in a single sortable string field. It’s not very practical for databases since it’s not easily readable, but it could be useful for some in-memory, queue-based algorithms.
Update June 15, 2025
A user reported collisions when adding records to a database using multiple threads. After running a few tests, I discovered the problem was Clarion’s RANDOM()
function, which apparently uses a simple call to rand
that doesn’t work well with multithreaded code.
The fix was easy: use BCryptGenRandom
. With this change I tested adding 1,000,000 records in 100 threads to a memory file in a few seconds without collisions.
Update June 16, 2025
Now that we have BCryptGenRandom
working in Clarion, we can easily leverage the fact that a LONG
is 4 bytes and use BCryptGenRandom
to fill those bytes with cryptographic-quality random data to generate a random integer, then scale it to a specific range, just like Clarion’s built-in function. The code is simple:
!!! <summary>
!!! Returns a cryptographically secure random integer.
!!! </summary>
!!! <param name="low">A numeric constant, variable, or expression for the lower boundary of the range.</param>
!!! <param name="high">A numeric constant, variable, or expression for the upper boundary of the range.</param>
!!! <returns>A random LONG integer between the low and high values, inclusively.</returns>
cRandom PROCEDURE(LONG low, LONG high)!,LONG
randomData STRING(4),AUTO
randomLong LONG,OVER(randomData)
range LONG,AUTO
CODE
BCryptGenRandom(0,randomData,4,BCRYPT_USE_SYSTEM_PREFERRED_RNG) !Get 4 cryptographic random bytes
range = high - low + 1
randomLong %= range
randomLong += low
RETURN randomLong
Performance testing shows cRandom
is approximately 24 times slower than RANDOM
, making it useful for scenarios where random distribution is more important than speed.
Code is available on GitHub.
To integrate into your application:
- Copy
bcryptrandom.lib
toaccessory\lib
or your app folder - Copy
cRandom.inc
andcRandom.clw
toaccessory\libsrc\win
or your app folder. - Add
INCLUDE('cRandom.inc'),ONCE
to your global map
This content was originally published on Clarion Hub 1 and 2.