A lot of verbiage for a couple of 1-line functions. This posting can be beneficial to usercontrol creators and anyone else that would like to reduce the number of variables that are used primarily for property values that contain ranges.
I think we all know that a Long value has 32 bits. Most of us also know that we can compress multiple separate values into a long value to save 'space'. For simplicity, we'll define 'space' as individual property settings including their variable declarations and any action required to persist/save those individual settings.
Let's first review different ways of looking at the 32 bits of a Long value
- 2 words, 1 word = 16 bits, 16 bits * 2 words = 32 bits
- 4 bytes, 1 byte = 8 bits, 8 bits * 4 bytes = 32 bits
- 8 nibbles, 1 nibble = 1/2 a byte = 4 bits, 4 bits * 8 nibbles = 32 bits
A word can hold 65536 different values, 0-65535 inclusively, 2^16 bits = 65536
A byte can hold 256 different values, 0-255 inclusively, 2^8 bits = 256
A nibble can hold 16 different values, 0-15 inclusively, 2^4 bits = 16
Another way to look at a Long is as a hexadecimal: FF FF FF FF. Each F is a nibble, each FF is a byte
(*) For our purposes, we will not be using Words. Since we are discussing saving multiple property settings into one Long value, it is highly unlikely you will have a property setting that will consist of 65K options.
So how many property settings can you jam into one Long value? It depends on how many options each property has and how many bits are needed to cover the range of those options. For example, if you had 32 boolean properties, you could fit all 32 into one Long value since a boolean only needs 1 bit: 0=false, 1=true. How do we determine how many bits are needed for the range of the property? A table is useful for those that hate to do the bit count and bit shifts needed.
Once you know how many bits are needed, you then need to find a location in the Long to place those bits. Simply put, you need consecutive bits available within the Long. The bits can wrap around from 1 nibble to another nibble. In this example we don't have any properties yet assigned to the Long, so 2 bits will fit into the first nibble, leaving 2 bits left in that nibble. If we had another property that required more than 2 bits, we can use the remaining 2 bits of the 1st nibble and the difference placed in the 2nd nibble. We could start in any nibble, on any bit, as long as consecutive bits are available. Do not include the high bit in any ranged property, only as a boolean value.
Now that you can determine how many bits are needed for each property and can determine which of your properties can fit into the Long, you need a bit mask to be able to locate those bits and shift those bits. The shifting is needed in order to place the bits in the proper location within the Long and also to extract those bits and return them to a value that fits within the range of the property options. Again, not all like the math involved with bit shifting, so we will kinda use some shortcuts.
You have bits counted for a property. What is its mask?
Instead of the steps below, you can copy & paste this into your immediate window instead, just fill in the 3 parameters
Step 1. Start with a blank mask, hexadecimal, so we are looking at each nibble: &H00000000. Each 0 is a nibble and the first nibble is the far right zero. Updating the mask starts with the zero that matches the the nibble position where the 1st bit of the property will be stored.
Step 2. Calculate the mask for that nibble and replace the zero with the hex code for the mask
Example
:: 2 bits needed to store the property and starting on nibble 1, bit position 3
:: Blank mask to start with, targeting nibble #1: &H00000000
:: Get mask for nibble. Starting on bit #3, needing 2 bits, mask is &HC. Update mask: &H0000000C
Step 3. If the bits needed to store the property have not all been placed in the mask, move to the next higher nibble, first bit position and repeat previous step until all bits have been accounted for. Example of wrap-around bits:
:: 6 bits needed to store the property and starting on nibble 5, bit position 4 (final bit in that nibble)
:: Blank mask to start with, targeting nibble #5: &H00000000
:: Get mask for nibble 5. Starting on final bit, so mask is &H8. Update mask: &H00080000
:: Get mask for nibble 6. Starting on 1st bit, all 4 bits needed. Update mask: &H00F80000
:: Get mask for nibble 7. Starting on 1st bit, 1 bit needed. Update mask: &H01F80000
(*) Note. If the mask ends up being first 4 nibbles and bit 4 of nibble 4 is used, you must append an ampersand to the end of mask else VB will treat it as a negative Integer value, i.e., &H8000&
And a quick example. Lets say we have a property that requires 3 bits and starts in nibble #6 at bit position #4. Using the steps above, the mask for the property would be &H3800000. The Long variable that holds the property settings is named: m_Properties
Tip: For boolean properties, it is easier to change the property directly using XOR than to call the above 1-liners. Those 1-liners can still be called, but you must convert the boolean True to an absolute value, not -1. Using XOR is really simple since you have the mask. Let's say the mask is &H80000000, using bit #4 in nibble #8
Note: If the high bit of the Long is used for a boolean property, it must be handled like the tip above. The 1-liners are not designed to handle the high bit. They can be modified to handle it via IFs.
Tip: If a boolean property is very often used in your code, it is a good strategy to have it occupy the high bit of the Long variable used to store the properties. Why? You can easily test to see if it is set by testing the sign of the Long variable, i.e., if m_Properties < 0 then the high bit is set. Easier than testing if (m_Properties And &H80000000) is non-zero.
Code:
Private Sub pvSetProperty(PackedValues As Long, PropMask As Long, NewValue As Long)
PackedValues = (PackedValues And Not PropMask) Or ((PropMask And -PropMask) * NewValue)
End Sub
Private Function pvGetProperty(PackedValues As Long, PropMask As Long)
pvGetProperty = (PackedValues And PropMask) \ (PropMask And -PropMask)
End Function
Let's first review different ways of looking at the 32 bits of a Long value
- 2 words, 1 word = 16 bits, 16 bits * 2 words = 32 bits
- 4 bytes, 1 byte = 8 bits, 8 bits * 4 bytes = 32 bits
- 8 nibbles, 1 nibble = 1/2 a byte = 4 bits, 4 bits * 8 nibbles = 32 bits
A word can hold 65536 different values, 0-65535 inclusively, 2^16 bits = 65536
A byte can hold 256 different values, 0-255 inclusively, 2^8 bits = 256
A nibble can hold 16 different values, 0-15 inclusively, 2^4 bits = 16
Another way to look at a Long is as a hexadecimal: FF FF FF FF. Each F is a nibble, each FF is a byte
(*) For our purposes, we will not be using Words. Since we are discussing saving multiple property settings into one Long value, it is highly unlikely you will have a property setting that will consist of 65K options.
So how many property settings can you jam into one Long value? It depends on how many options each property has and how many bits are needed to cover the range of those options. For example, if you had 32 boolean properties, you could fit all 32 into one Long value since a boolean only needs 1 bit: 0=false, 1=true. How do we determine how many bits are needed for the range of the property? A table is useful for those that hate to do the bit count and bit shifts needed.
Range ... Bits Needed
0-1 ... 1 :: 2^1-1 = 0 to 1, 2 options max, i.e., False or True
0-3 ... 2 :: 2^2-1 = 0 to 3, 4 options max
0-7 ... 3 :: 2^3-1 = 0 to 7, 8 options max
0-15 ... 4 :: 2^4-1 = 0 to 15, 16 options max
0-31 ... 5 :: 2^5-1 = 0 to 31, 32 options max
0-63 ... 6 :: 2^6-1 = 0 to 63, 64 options max
0-127 ... 7 :: 2^7-1 = 0 to 127, 128 options max
0-255 ... 8 :: 2^8-1 = 0 to 255, 256 options max
(*) Caveat. For the functions provided above, the high bit (bit 4 in nibble 8) of the Long can only be used for a boolean value. It must never be part of a property that requires more than 1 bit. Nor must the high bit be part of the mask sent to those functions. This is simply to keep the functions simple vs. dealing with the toggling of the sign bit. For ranged properties, consider a Long as a maximum of 31 available bits. Boolean properties can use the functions posted above, but it is easier to toggle them directly. That is also shown below as the 'tip" at end of posting.0-1 ... 1 :: 2^1-1 = 0 to 1, 2 options max, i.e., False or True
0-3 ... 2 :: 2^2-1 = 0 to 3, 4 options max
0-7 ... 3 :: 2^3-1 = 0 to 7, 8 options max
0-15 ... 4 :: 2^4-1 = 0 to 15, 16 options max
0-31 ... 5 :: 2^5-1 = 0 to 31, 32 options max
0-63 ... 6 :: 2^6-1 = 0 to 63, 64 options max
0-127 ... 7 :: 2^7-1 = 0 to 127, 128 options max
0-255 ... 8 :: 2^8-1 = 0 to 255, 256 options max
Once you know how many bits are needed, you then need to find a location in the Long to place those bits. Simply put, you need consecutive bits available within the Long. The bits can wrap around from 1 nibble to another nibble. In this example we don't have any properties yet assigned to the Long, so 2 bits will fit into the first nibble, leaving 2 bits left in that nibble. If we had another property that required more than 2 bits, we can use the remaining 2 bits of the 1st nibble and the difference placed in the 2nd nibble. We could start in any nibble, on any bit, as long as consecutive bits are available. Do not include the high bit in any ranged property, only as a boolean value.
Now that you can determine how many bits are needed for each property and can determine which of your properties can fit into the Long, you need a bit mask to be able to locate those bits and shift those bits. The shifting is needed in order to place the bits in the proper location within the Long and also to extract those bits and return them to a value that fits within the range of the property options. Again, not all like the math involved with bit shifting, so we will kinda use some shortcuts.
You have bits counted for a property. What is its mask?
Instead of the steps below, you can copy & paste this into your immediate window instead, just fill in the 3 parameters
Code:
' if bits shift into the high bit, overflow error will occur
' bitPos is where in the nibble the first bit will be stored: 1-4
' nrBits is total number of bits for the value: 1-31
' nibble is the one where start of value is stored: 1-8
' FYI: nibble 8, bit position 4 is: &H80000000
bitPos=1:nrBits=1:nibble=1:? "&H" & Hex(((2^nrBits-1)*2^(bitPos-1))*(16^(nibble-1))) & IIF((nibble-1)*4+nrBits+bitPos-1=16,"&","")
Step 2. Calculate the mask for that nibble and replace the zero with the hex code for the mask
Start bit position in nibble, number of bits used for that nibble, mask needed
Bit position 1 :: 1 bit used &H1, 2 bits used &H3, 3 bits used &H7, 4 bits used &HF
Bit position 2 :: 1 bit used &H2, 2 bits used &H6, 3 bits used &HE, just 3 bits remain from position #2
Bit position 3 :: 1 bit used &H4, 2 bits used &HC, just 2 bits remain from position #3
Bit position 4 :: always &H8 since only 1 bit is available at position #4
Bit position 1 :: 1 bit used &H1, 2 bits used &H3, 3 bits used &H7, 4 bits used &HF
Bit position 2 :: 1 bit used &H2, 2 bits used &H6, 3 bits used &HE, just 3 bits remain from position #2
Bit position 3 :: 1 bit used &H4, 2 bits used &HC, just 2 bits remain from position #3
Bit position 4 :: always &H8 since only 1 bit is available at position #4
:: 2 bits needed to store the property and starting on nibble 1, bit position 3
:: Blank mask to start with, targeting nibble #1: &H00000000
:: Get mask for nibble. Starting on bit #3, needing 2 bits, mask is &HC. Update mask: &H0000000C
Step 3. If the bits needed to store the property have not all been placed in the mask, move to the next higher nibble, first bit position and repeat previous step until all bits have been accounted for. Example of wrap-around bits:
:: 6 bits needed to store the property and starting on nibble 5, bit position 4 (final bit in that nibble)
:: Blank mask to start with, targeting nibble #5: &H00000000
:: Get mask for nibble 5. Starting on final bit, so mask is &H8. Update mask: &H00080000
:: Get mask for nibble 6. Starting on 1st bit, all 4 bits needed. Update mask: &H00F80000
:: Get mask for nibble 7. Starting on 1st bit, 1 bit needed. Update mask: &H01F80000
(*) Note. If the mask ends up being first 4 nibbles and bit 4 of nibble 4 is used, you must append an ampersand to the end of mask else VB will treat it as a negative Integer value, i.e., &H8000&
And a quick example. Lets say we have a property that requires 3 bits and starts in nibble #6 at bit position #4. Using the steps above, the mask for the property would be &H3800000. The Long variable that holds the property settings is named: m_Properties
Code:
Public Property Get WidgetStyle() As WidgetStyleEnum
WidgetValue = pvGetProperty(m_Properties, &H3800000)
End Property
Public Property Let WidgetStyle(Value As WidgetStyleEnum)
' validate Value is within the range of WidgetStyleEnum else abort
pvSetProperty m_Properties, &H3800000, Value
End Property
Code:
Public Property Get AutoSize() As Boolean
AutoSize = (m_Properties And &H80000000)
End Property
Public Property Let AutoSize(Value As Boolean)
If Not Value = Me.AutoSize Then
m_Properties = m_Properties Xor &H80000000
End If
End Property
Tip: If a boolean property is very often used in your code, it is a good strategy to have it occupy the high bit of the Long variable used to store the properties. Why? You can easily test to see if it is set by testing the sign of the Long variable, i.e., if m_Properties < 0 then the high bit is set. Easier than testing if (m_Properties And &H80000000) is non-zero.