XojoScript Dictionary

But is it relevant/mandatory to retrieve the Keys and the Values in the order they were entered ? What counts is that both the Keys array and the Values array are in sync, right ? (granted there has been no modification to the Dictionary in-between).

You ask an excellent question, and I was trying to emulate the native Dictionary in that respect. I also feel like I’ve taken advantage of this in the past.

If I change it now, I’d have to redesign most of it starting with introducing a Pair class. The bin would change to an array of Pair, and each Pair would hold an array of keys on the left and an array of values on the right. To grab an array of Keys or Values, I’d have to cycle through the entire bin looking for non-nil values. As I find them, I’d add the Keys/Values from the contents to the array.

I’ll have to think about whether it’s worth it. The scheme seems like it would be more memory-efficient, but ultimately slower.

I’m not sure the native Dictionary ensures that it returns the Keys or Values in the order they were entered. I don’t see that from the documentation (or perhaps this was mentioned by a Xojo developer in a thread or a blog ?). What it ensures is to have the Keys and Values in-sync (as long as the Dictionary is not modified of course).

Regarding cycling through the entire bin looking for non-nil values, you could create a helper array were you append the indices of the bin occupied slots. This way you would just have to scan this helper array instead of the entire bin.

Regarding the bin change I would have thought to keep it as an array of array, as before, but instead of storing indices in the secondary array I would have stored straight Pairs of Keys/Values. I understand you would then have to grab the Keys or Values one by one to reconstitute an array, but I’m thinking more about the Value()/Lookup() efficiency than the Keys()/Values() efficiency, I have to admit.

On a second thought, perhaps it would be more efficient to let Instr to find the Key in the array, with your first idea of storing the arrays of Keys and Values on Left/Right of a Pair, instead of having an array of Pairs.

I did a small (very small) test to see if the Keys maintain their order of entry in the native Dictionary even after removal of previous keys, and they do. But I can’t find the documentation on this either, so perhaps that’s not always true or subject to change. As you say, it’s probably not that important.

I think my plan would be more efficient for Value()/Lookup() too since I could then leverage IndexOf on the key array within the Pair.

binIndex = key.Hash mod ( zBin.Ubound + 1 )
dim p As Pair = zBin( binIndex )
if p <> nil then
  dim k() As Variant = p.Left
  dim keyIndex as Integer = k.IndexOf( key )
  if keyIndex <> -1 then // found it
    …
  end if
end if

I agree, yes. (I meant IndexOf indeed, not Instr…)

Now I have to think about whether I really want to refactor this. :slight_smile: One advantage is that a nil key would be acceptable. Since my scheme changes a key to nil to mark it “removed”, that’s currently not permitted.

In the meantime, here is the latest version, including unit tests. This adds a Constructor that lets you define the BinCount (you can’t change the existing BinCount as you can with the native Dictionary), and trims the Key/Value arrays if possible when keys are removed.

  Class Dictionary_MTC
    Const Version = 1.1
    Private Const kDefaultBinUbound = 4999
    
    Private zKeys() As Variant
    Private zValues() As Variant 
    Private zBin() As Variant
    Private zRemovedCount As Integer
    Private zLastBinIndex As Integer
    Private zBinUbound As Integer
    
    Private Function pGetKeyIndex ( key As Variant ) As Integer
      if key = nil then return -1
      
      zLastBinIndex = key.Hash mod ( zBin.Ubound + 1 )
      if zBin( zLastBinIndex ) is nil then
        return -1
      end if
      
      dim arr() As Integer = zBin( zLastBinIndex )
      
      dim i as Integer
      for i = 0 to arr.Ubound
        dim keyIndex as Integer
        keyIndex = arr( i )
        
        if zKeys( keyIndex ) = key then
          return keyIndex
        end if
      next i
      
      return -1
    End Function
    
    Private Sub pRaiseException()
      // Just raise a NilObjectException for now
      dim arr() as Integer
      arr = nil
      dim i as Integer = arr.IndexOf( 1 )
    End Sub
    
    Sub Value ( key As Variant, Assigns value As Variant )
      if key = nil then pRaiseException()
      
      dim keyIndex as Integer = pGetKeyIndex( key )
      if keyIndex <> -1 then
        zValues( keyIndex ) = value // Overwrite the existing value
      else
        zKeys.Append key
        zValues.Append value
        
        dim binIndex as Integer = zLastBinIndex
        dim arr() as Integer
        if zBin( binIndex ) <> nil then
          arr = zBin( binIndex )
        end if
        arr.Append zKeys.Ubound
        zBin( binIndex ) = arr
      end if
    End Sub
    
    Sub Constructor ( binCount As Integer = 0 )
      if binCount < 1 then
        zBinUbound = kDefaultBinUbound
      else
        zBinUbound = binCount - 1
      end if
      redim zBin( zBinUbound )
    End Sub
    
    Function Value ( key As Variant ) As Variant
      if key = nil then pRaiseException()
      
      dim keyIndex as Integer = pGetKeyIndex( key )
      if keyIndex = -1 then
        pRaiseException()
        return nil
      end if
      
      return zValues( keyIndex )
    End Function
    
    Function Lookup ( key As Variant, default As Variant ) As Variant
      if key = nil then pRaiseException()
      
      dim keyIndex as Integer = pGetKeyIndex( key )
      if keyIndex = -1 then
        return default
      else
        return zValues( keyIndex )
      end if
    End Function
    
    Function HasKey ( key As Variant ) As Boolean
      if key = nil then pRaiseException()
      return pGetKeyIndex( key ) <> -1
    End Function
    
    Function Count() As Integer
      return zKeys.Ubound + 1 - zRemovedCount
    End Function
    
    Function Keys() As Variant()
      dim k() as Variant
      
      dim i as Integer
      for i = 0 to zKeys.Ubound
        dim thisKey as Variant = zKeys( i )
        if thisKey <> nil then
          k.Append thisKey
        end if
      next i
      return k
    End Function
    
    Function Values() As Variant()
      dim v() as Variant
      
      dim i as Integer
      for i = 0 to zValues.Ubound
        if zKeys( i ) <> nil then
          v.Append zValues( i )
        end if
      next i
      return v
    End Function
    
    Sub Remove ( key As Variant )
      if key = nil then pRaiseException()
      
      dim keyIndex as Integer = pGetKeyIndex( key )
      if keyIndex = -1 then pRaiseException()
      
      zKeys( keyIndex ) = nil
      zValues( keyIndex ) = nil
      zRemovedCount = zRemovedCount + 1
      
      dim arr() as Integer = zBin( zLastBinIndex )
      arr.Remove arr.IndexOf( keyIndex )
      zBin( zLastBinIndex ) = arr 
      
      // Trim the arrays
      dim lastGoodIndex as Integer = zKeys.Ubound
      while lastGoodIndex <> -1 and zKeys( lastGoodIndex ) is nil
        lastGoodIndex = lastGoodIndex - 1
        zRemovedCount = zRemovedCount - 1
      wend
      if zKeys.Ubound <> lastGoodIndex then
        redim zKeys( lastGoodIndex )
        redim zValues( lastGoodIndex )
      end if
    End Sub
    
    Function BinCount() As Integer
      return zBinUbound + 1
    End Function
    
    Sub Clear()
      redim zKeys( -1 )
      redim zValues( -1 )
      redim zBin( -1 )
      redim zBin( zBinUbound )
      zRemovedCount = 0
    End Sub
  End Class
  
  // Unit Tests
  Function PrintIfNot ( condition As Boolean, msg As String ) As Boolean
    dim printIt as Boolean = not condition
    if printIt then
      print msg
    end if
    return printIt
  End Function
  
  // TEST CODE
  dim failed as boolean
  
  dim d as Dictionary_MTC
  d = new Dictionary_MTC
  
  d.Value( 1 ) = 2
  failed = PrintIfNot( d.Value( 1 ) = 2, "The value 2 was not stored in key1" ) or failed
  d.Value( 1 ) = 3
  failed = PrintIfNot( d.Value( 1 ) = 3, "The value for key 1 was not overwritten" ) or failed
  
  failed = PrintIfNot( d.Lookup( 1, 88 ) = 3, "Lookup of existing key failed" ) or failed
  failed = PrintIfNot( d.Lookup( 2, 99 ) = 99, "Loojup of unknown key failed" ) or failed
  
  failed = PrintIfNot( d.HasKey( 1 ), "HasKey failed with existing key" ) or failed
  failed = PrintIfNot( not d.HasKey( 2 ), "HasKey failed with unknown key" ) or failed
  
  failed = PrintIfNot( d.Count = 1, "Count should be 1, instead is " + Str( d.Count ) ) or failed
  
  d.Remove( 1 )
  failed = PrintIfNot( d.Count = 0, "Remove has failed. Count is " + Str( d.Count ) ) or failed
  
  try
    print d.Value( 1 ) // Exception
    print "An exception should have been generated looking up an unknown key"
    failed = true
  catch
    // What we expect so keep going
  end try
  
  dim k() as Variant
  k = d.Keys
  failed = PrintIfNot( k.Ubound = -1, "Got back " + Str( k.Ubound ) + " Keys even though the Dictionary should be empty" ) or failed
  
  d.Value( "s" ) = 1
  d.Value( "S" ) = 2
  failed = PrintIfNot( d.Value( "s" ) = 2 and d.Value( "S" ) = 2, "Case-insensitivity seems to have been lost" ) or failed
  
  dim index as Integer
  dim startTime, endTime as Double
  const kLoops = 10000
  
  d = new Dictionary_MTC( kLoops * 2 )
  startTime = microseconds
  for index = 1 to kLoops
    d.Value( Str( index ) ) = index
  next index
  endTime = microseconds
  print "Storage took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  
  dim retrieved as Variant
  startTime = microseconds
  for index = 1 to kLoops
    retrieved = d.Value( Str( index ) )
    if retrieved <> index then
      failed = true
      print "Retrival failed"
      exit
    end if
  next index
  endTime = microseconds
  print "Retrieval took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  
  if FALSE then
    // Compare to array
    dim keyArr( kLoops ) as String
    dim valueArr( kLoops ) As Integer
    for index =  1 to kLoops
      keyArr( index ) = Str( index )
      valueArr( index ) = index
    next index
    
    startTime = microseconds
    for index = 1 to kLoops
      retrieved = valueArr( keyArr.IndexOf( Str( index ) ) )
      if retrieved <> index then
        print "Array lookup failed?"
        exit
      end if
    next index
    endTime = microseconds
    print "Array lookup took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  end if
  
  if not failed then
    print "ALL TESTS HAVE PASSED!"
  end if

Like you found in the documentation, Dictionary explicitly does not guarantee that items in the Keys/Values arrays are returned in any specific order.

[quote=82935:@Guy Rabiller]Paul, forgive me, I don’t want to sound harsh here but your Dictionary implementation is inefficient.
[/quote]

Yeh. I guess I used my usual ploy of:

  1. Get it working.
  2. Make it work efficiently.

but never got around to step 2.

In my application, efficiency is not paramount.

I reworked it based on this conversation. I created a private class called DictionaryBinEntry with two properties, Keys() As Variant and Values() As Variant. The zBin array is now an array of that class. Everything else is worked out around that. This turns out to be a little faster than the first implementation, although it’s still much slower than the native Dictionary.

Ironically, I couldn’t use IndexOf because Variant.IndexOf considers case (case-insensitivity is lost).

  Module MTCUtils
    Private Class DictionaryBinEntry
    Dim Keys() As Variant
    Dim Values() As Variant
    End Class
    
    Class Dictionary_MTC
      Const Version = 2.0
      Private Const kDefaultBinUbound = 4999
      
      Private zBin() As DictionaryBinEntry
      Private zBinUbound As Integer
      Private zCount As Integer
      
      // Private Methods
      Private Function pBinIndex ( key As Variant ) as Integer
        return key.Hash mod ( zBin.Ubound + 1 )
      End Function
      
      Private Function pBinEntry ( key As Variant, ByRef binIndex As Integer ) As DictionaryBinEntry
        binIndex  = pBinIndex( key )
        return zBin( binIndex )
      End Function
      
      Private Function pBinEntry ( key As Variant ) As DictionaryBinEntry
        dim binIndex as Integer = pBinIndex( key )
        return zBin( binIndex )
      End Function
      
      Private Function pIndexOf ( variantArr() As Variant, value As Variant ) As Integer
        // Need this because native IndexOf is case-sensitive with Variants
        dim r as Integer = -1
        dim i as integer
        for i = 0  to variantArr.Ubound
          if variantArr( i ) = value then
            r = i
            exit
          end if
        next i
        return r
      End Function
      
      // Constructor
      Sub Constructor ( binCount As Integer = 0 )
        if binCount < 1 then
          zBinUbound = kDefaultBinUbound
        else
          zBinUbound = binCount - 1
        end if
        redim zBin( zBinUbound )
      End Sub
      
      // Value
      Function Value ( key As Variant ) As Variant
        dim binEntry as DictionaryBinEntry = pBinEntry( key )
        dim keyIndex as Integer = pIndexOf( binEntry.Keys, key )
        return binEntry.Values( keyIndex )
      End Function
      
      Sub Value ( key As Variant, Assigns value As Variant )
        dim binIndex as Integer
        dim binEntry as DictionaryBinEntry = pBinEntry( key, binIndex )
        
        dim keyIndex as Integer
        if binEntry is nil then
          binEntry = new DictionaryBinEntry
          keyIndex = -1
        else
          keyIndex = pIndexOf( binEntry.Keys, key )
        end if
        
        if keyIndex = -1 then // New entry
          binEntry.Keys.Append key
          binEntry.Values.Append value
          zCount = zCount + 1
        else
          binEntry.Values( keyIndex ) = value
        end if
        
        zBin( binIndex ) = binEntry
      End Sub
      
      // Lookup
      Function Lookup ( key As Variant, default As Variant ) As Variant
        dim binEntry as DictionaryBinEntry = pBinEntry( key )
        if binEntry is nil or binEntry.Keys.Ubound = -1 then return default
        dim keyIndex as Integer = pIndexOf( binEntry.Keys, key )
        if keyIndex = -1 then
          return default
        else
          return binEntry.Values( keyIndex )
        end if
      End Function
      
      // HasKey
      Function HasKey ( key As Variant ) As Boolean
        dim binEntry as DictionaryBinEntry = pBinEntry( key )
        if binEntry is nil or binEntry.Keys.Ubound = -1 then return false
        return pIndexOf( binEntry.Keys, key ) <> -1
      End Function
      
      // Remove
      Sub Remove ( key As Variant )
        dim binIndex as Integer
        dim binEntry as DictionaryBinEntry = pBinEntry( key, binIndex )
        dim keyIndex as integer = pIndexOf( binEntry.Keys, key )
        binEntry.Keys.Remove keyIndex
        binEntry.Values.Remove keyIndex
        zCount = zCount - 1
      End Sub
      
      // Keys
      Function Keys() As Variant
        dim k() as Variant
        dim binEntry as DictionaryBinEntry
        dim binIndex as Integer
        for binIndex = 0 to zBin.Ubound
          binEntry = zBin( binIndex )
          if binEntry <> nil then
            dim allKeys() as Variant = binEntry.Keys
            dim keyIndex as Integer
            for keyIndex = 0 to allKeys.Ubound
              k.Append allKeys( keyIndex )
            next keyIndex
          end if
        next binIndex
        return k()
      End Function
      
      // Values
      Function Values() As Variant
        dim v() as Variant
        dim binEntry as DictionaryBinEntry
        dim binIndex as Integer
        for binIndex = 0 to zBin.Ubound
          binEntry = zBin( binIndex )
          if binEntry <> nil then
            dim allKeys() as Variant = binEntry.Values
            dim valueIndex as Integer
            for valueIndex = 0 to allKeys.Ubound
              v.Append allKeys( valueIndex )
            next valueIndex
          end if
        next binIndex
        return v()
      End Function
      
      // Count
      Function Count() As Integer
        return zCount
      End Function
      
      // Clear
      Sub Clear()
        redim zBin( -1 )
        redim zBin( zBinUbound )
        zCount = 0
      End Sub
      
    End Class
  End Module
  
  // Unit Tests
  Function PrintIfNot ( condition As Boolean, msg As String ) As Boolean
    dim printIt as Boolean = not condition
    if printIt then
      print msg
    end if
    return printIt
  End Function
  
  // TEST CODE
  dim failed as boolean
  
  dim d as Dictionary_MTC
  d = new Dictionary_MTC
  
  d.Value( 1 ) = 2
  failed = PrintIfNot( d.Value( 1 ) = 2, "The value 2 was not stored in key1" ) or failed
  d.Value( 1 ) = 3
  failed = PrintIfNot( d.Value( 1 ) = 3, "The value for key 1 was not overwritten" ) or failed
  
  failed = PrintIfNot( d.Lookup( 1, 88 ) = 3, "Lookup of existing key failed" ) or failed
  failed = PrintIfNot( d.Lookup( 2, 99 ) = 99, "Loojup of unknown key failed" ) or failed
  
  failed = PrintIfNot( d.HasKey( 1 ), "HasKey failed with existing key" ) or failed
  failed = PrintIfNot( not d.HasKey( 2 ), "HasKey failed with unknown key" ) or failed
  
  failed = PrintIfNot( d.Count = 1, "Count should be 1, instead is " + Str( d.Count ) ) or failed
  
  d.Remove( 1 )
  failed = PrintIfNot( d.Count = 0, "Remove has failed. Count is " + Str( d.Count ) ) or failed
  
  try
    print d.Value( 1 ) // Exception
    print "An exception should have been generated looking up an unknown key"
    failed = true
  catch
    // What we expect so keep going
  end try
  
  dim k() as Variant
  k = d.Keys
  failed = PrintIfNot( k.Ubound = -1, "Got back " + Str( k.Ubound ) + " Keys even though the Dictionary should be empty" ) or failed
  
  d.Value( "s" ) = 1
  d.Value( "S" ) = 2
  failed = PrintIfNot( d.Value( "s" ) = 2 and d.Value( "S" ) = 2, "Case-insensitivity seems to have been lost" ) or failed
  failed = PrintIfNot( d.Count = 1, "Count should be 1 but is " + Str( d.Count ) ) or failed
  d.Value( "f" ) = 3
  d.Value( "e" ) = 4
  failed = PrintIfNot( d.Count = 3, "Count should be 3 but is " + Str( d.Count ) ) or failed
  
  k = d.Keys
  failed = PrintIfNot( k.Ubound = 2, "Ubound for Keys should be 2 but is " + Str( k.Ubound ) ) or failed
  dim result as string
  dim index as Integer
  for index = 0 to k.Ubound
    result = result + k( index )
  next index
  failed = PrintIfNot( result = "efs", "Result of Keys doesn't match: " + result ) or failed
  
  dim v() as Variant
  v = d.Values
  failed = PrintIfNot( v.Ubound = 2, "UBound for Values should be 2 but is " + Str( v.Ubound ) ) or failed
  result = ""
  for index = 0 to v.Ubound
    result = result + v( index )
  next index
  failed = PrintIfNot( result = "432", "Result of Values doesn't match: " + result ) or failed
  
  d.Remove( "E" )
  failed = PrintIfNot( d.Count = 2, "Count should be 2 but is " + Str( d.Count ) ) or failed
  
  dim startTime, endTime as Double
  const kLoops = 10000
  
  d = new Dictionary_MTC( kLoops * 2 )
  startTime = microseconds
  for index = 1 to kLoops
    d.Value( Str( index ) ) = index
  next index
  endTime = microseconds
  print "Storage took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  
  dim retrieved as Variant
  startTime = microseconds
  for index = 1 to kLoops
    retrieved = d.Value( Str( index ) )
    if retrieved <> index then
      failed = true
      print "Retrieval failed"
      exit
    end if
  next index
  endTime = microseconds
  print "Retrieval took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  
  if FALSE then
    // Compare to array
    dim keyArr( kLoops ) as String
    dim valueArr( kLoops ) As Integer
    for index =  1 to kLoops
      keyArr( index ) = Str( index )
      valueArr( index ) = index
    next index
    
    startTime = microseconds
    for index = 1 to kLoops
      retrieved = valueArr( keyArr.IndexOf( Str( index ) ) )
      if retrieved <> index then
        print "Array lookup failed?"
        exit
      end if
    next index
    endTime = microseconds
    print "Array lookup took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  end if
  
  if not failed then
    print "ALL TESTS HAVE PASSED!"
  end if

I was thinking about another strategy, If I have the time I may test it as well:

The idea would be to keep the original Arrays of Arrays, but with more Arrays, and ultimately directly storing a Key/Value Pair, without the need for IndexOf or for scanning any Array, just using hash each time.

  1. As before, compute a hash to get an index in the main bin array.
  2. Check if, at that index, you have an Array or a Pair or Nil.
    2a) If Nil, just store the Pair.
    2b) If Pair, you have collision, then create an Array. and go to step 2c)
    2c) If Array, then use another Hash computation to store the Pair in that Array.
  3. Whenever there is a collision (even at step 2c), continue to create Arrays and use Hash computation again to store Pairs.

bin : (a,b,c,…)

a: Nil or
a: Pair(Key/Value) or
a: (aa,ba,ca, …)

aa: Nil or
aa: Pair(Key/Value) or
aa: (aaa,baa,caa,…) or

etc…

This way you never scan an Array, you go directly to a slot through Hash values and Arrays of Arrays of Arrays of … until you get the Pair(Key/Value).

This may be a bit less Memory efficient as you have to create empty Arrays at each ‘level’ (although the length of the Array can diminish at each level) but this may leads to substantial gain in speed, well in theory…

This would not help in gathering the Keys and Values Arrays though, but personally I’m more concerned about the Value/Lookup performance.

(In fact, I’m thinking that storing Keys and Values could be done by the user of the Dictionary using Arrays outside of the Dictionary, which would discharge the Dictionary of a lot of useless troubles and allows it to focus on its main purpose: fast Key lookup.)

I’d be interested to know the results of your tests.

In the meantime, I added to reporting features to my version to aid in analysis, Collision and MostCollisions.

In my test code where I store 10,000 items, if I don’t change the default bin count of 5,000 (Ubound 4999), I get 5090 collisions, but the most collision in any one bin is 5 (meaning that 6 items are stored in that bin).

If I change the bin count to 20,000 (twice the number of items stored), collisions drop to 2,694, and the most in any one bin is 3. If I use 20,001 (an odd number), collisions drop to 1,509 and the most is 1. HOWEVER, while increasing the bin count helps over the default, the lookup times are otherwise unaffected.

My fear with your scheme is that all the extra processing and conversions will take more time than actually looking up the values. However, as I said, I’d be curious to know.

Here is the latest version. I made the default bin count an odd number and moved the code that scans variants so it can be available outside the module.

  Module MTCUtils
    
    Protected Function VariantIndexOf ( variantArr() As Variant, value As Variant ) As Integer
      // Need this because native IndexOf is case-sensitive with Variants
      dim i as integer
      for i = 0  to variantArr.Ubound
        if variantArr( i ) = value then
          return i
        end if
      next i
      return -1 // Not found
    End Function
    
    Private Class DictionaryBinEntry
    Dim Keys() As Variant
    Dim Values() As Variant
    End Class
    
    Class Dictionary_MTC
      Const Version = 2.1
      Private Const kDefaultBinUbound = 5000 // Makes Bin Count an odd number
      
      Private zBin() As DictionaryBinEntry
      Private zBinUbound As Integer
      Private zBinCount As Integer
      Private zCount As Integer
      
      // Private Methods
      Private Function pBinIndex ( key As Variant ) as Integer
        return key.Hash mod zBinCount
      End Function
      
      Private Function pBinEntry ( key As Variant, ByRef binIndex As Integer ) As DictionaryBinEntry
        binIndex  = pBinIndex( key )
        return zBin( binIndex )
      End Function
      
      Private Function pBinEntry ( key As Variant ) As DictionaryBinEntry
        dim binIndex as Integer = pBinIndex( key )
        return zBin( binIndex )
      End Function
      
      // Constructor
      Sub Constructor ( binCount As Integer = 0 )
        if binCount < 1 then
          zBinUbound = kDefaultBinUbound
        else
          zBinUbound = binCount - 1
        end if
        redim zBin( zBinUbound )
        zBinCount = zBinUbound + 1
      End Sub
      
      // Value
      Function Value ( key As Variant ) As Variant
        dim binEntry as DictionaryBinEntry = pBinEntry( key )
        dim keyIndex as Integer = VariantIndexOf( binEntry.Keys, key )
        return binEntry.Values( keyIndex )
      End Function
      
      Sub Value ( key As Variant, Assigns value As Variant )
        dim binIndex as Integer
        dim binEntry as DictionaryBinEntry = pBinEntry( key, binIndex )
        
        dim keyIndex as Integer
        if binEntry is nil then
          binEntry = new DictionaryBinEntry
          keyIndex = -1
        else
          keyIndex = VariantIndexOf( binEntry.Keys, key )
        end if
        
        if keyIndex = -1 then // New entry
          binEntry.Keys.Append key
          binEntry.Values.Append value
          zCount = zCount + 1
        else
          binEntry.Values( keyIndex ) = value
        end if
        
        zBin( binIndex ) = binEntry
      End Sub
      
      // Lookup
      Function Lookup ( key As Variant, default As Variant ) As Variant
        dim binEntry as DictionaryBinEntry = pBinEntry( key )
        if binEntry is nil or binEntry.Keys.Ubound = -1 then return default
        dim keyIndex as Integer = VariantIndexOf( binEntry.Keys, key )
        if keyIndex = -1 then
          return default
        else
          return binEntry.Values( keyIndex )
        end if
      End Function
      
      // HasKey
      Function HasKey ( key As Variant ) As Boolean
        dim binEntry as DictionaryBinEntry = pBinEntry( key )
        if binEntry is nil or binEntry.Keys.Ubound = -1 then return false
        return VariantIndexOf( binEntry.Keys, key ) <> -1
      End Function
      
      // Remove
      Sub Remove ( key As Variant )
        dim binIndex as Integer
        dim binEntry as DictionaryBinEntry = pBinEntry( key, binIndex )
        dim keyIndex as integer = VariantIndexOf( binEntry.Keys, key )
        binEntry.Keys.Remove keyIndex
        binEntry.Values.Remove keyIndex
        zCount = zCount - 1
      End Sub
      
      // Keys
      Function Keys() As Variant
        dim k() as Variant
        dim binEntry as DictionaryBinEntry
        dim binIndex as Integer
        for binIndex = 0 to zBin.Ubound
          binEntry = zBin( binIndex )
          if binEntry <> nil then
            dim allKeys() as Variant = binEntry.Keys
            dim keyIndex as Integer
            for keyIndex = 0 to allKeys.Ubound
              k.Append allKeys( keyIndex )
            next keyIndex
          end if
        next binIndex
        return k()
      End Function
      
      // Values
      Function Values() As Variant
        dim v() as Variant
        dim binEntry as DictionaryBinEntry
        dim binIndex as Integer
        for binIndex = 0 to zBin.Ubound
          binEntry = zBin( binIndex )
          if binEntry <> nil then
            dim allKeys() as Variant = binEntry.Values
            dim valueIndex as Integer
            for valueIndex = 0 to allKeys.Ubound
              v.Append allKeys( valueIndex )
            next valueIndex
          end if
        next binIndex
        return v()
      End Function
      
      // Count
      Function Count() As Integer
        return zCount
      End Function
      
      // Clear
      Sub Clear()
        redim zBin( -1 )
        redim zBin( zBinUbound )
        zCount = 0
      End Sub
      
      // Reporting
      // Collisions (report how many collisions have occurred)
      Function Collisions() As Integer
        dim r as Integer
        dim binEntry as DictionaryBinEntry
        for each binEntry in zBin
          if binEntry <> nil then
            dim cnt as Integer = binEntry.Keys.Ubound
            if cnt > 0 then r = r + cnt
          end if
        next binEntry
        return r
      End Function
      
      Function MostCollisions() As Integer
        dim r as Integer 
        dim binEntry as DictionaryBinEntry
        for each binEntry in zBin
          if binEntry <> nil then
            dim cnt as Integer = binEntry.Keys.Ubound
            if cnt > r then r = cnt
          end if
        next binEntry
        return r
      End Function
      
    End Class
  End Module
  
  // ----------------- END MODULE -----------------------

Since it’s too long for one post now, here are the unit tests:

  // Unit Tests
  Function PrintIfNot ( condition As Boolean, msg As String ) As Boolean
    dim printIt as Boolean = not condition
    if printIt then
      print msg
    end if
    return printIt
  End Function
  
  dim failed as boolean
  
  dim d as Dictionary_MTC
  d = new Dictionary_MTC
  
  d.Value( 1 ) = 2
  failed = PrintIfNot( d.Value( 1 ) = 2, "The value 2 was not stored in key1" ) or failed
  d.Value( 1 ) = 3
  failed = PrintIfNot( d.Value( 1 ) = 3, "The value for key 1 was not overwritten" ) or failed
  
  failed = PrintIfNot( d.Lookup( 1, 88 ) = 3, "Lookup of existing key failed" ) or failed
  failed = PrintIfNot( d.Lookup( 2, 99 ) = 99, "Loojup of unknown key failed" ) or failed
  
  failed = PrintIfNot( d.HasKey( 1 ), "HasKey failed with existing key" ) or failed
  failed = PrintIfNot( not d.HasKey( 2 ), "HasKey failed with unknown key" ) or failed
  
  failed = PrintIfNot( d.Count = 1, "Count should be 1, instead is " + Str( d.Count ) ) or failed
  
  d.Remove( 1 )
  failed = PrintIfNot( d.Count = 0, "Remove has failed. Count is " + Str( d.Count ) ) or failed
  
  try
    print d.Value( 1 ) // Exception
    print "An exception should have been generated looking up an unknown key"
    failed = true
  catch
    // What we expect so keep going
  end try
  
  dim k() as Variant
  k = d.Keys
  failed = PrintIfNot( k.Ubound = -1, "Got back " + Str( k.Ubound ) + " Keys even though the Dictionary should be empty" ) or failed
  
  d.Value( "s" ) = 1
  d.Value( "S" ) = 2
  failed = PrintIfNot( d.Value( "s" ) = 2 and d.Value( "S" ) = 2, "Case-insensitivity seems to have been lost" ) or failed
  failed = PrintIfNot( d.Count = 1, "Count should be 1 but is " + Str( d.Count ) ) or failed
  d.Value( "f" ) = 3
  d.Value( "e" ) = 4
  failed = PrintIfNot( d.Count = 3, "Count should be 3 but is " + Str( d.Count ) ) or failed
  
  k = d.Keys
  failed = PrintIfNot( k.Ubound = 2, "Ubound for Keys should be 2 but is " + Str( k.Ubound ) ) or failed
  dim result as string
  dim index as Integer
  for index = 0 to k.Ubound
    result = result + k( index )
  next index
  failed = PrintIfNot( result = "efs", "Result of Keys doesn't match: " + result ) or failed
  
  dim v() as Variant
  v = d.Values
  failed = PrintIfNot( v.Ubound = 2, "UBound for Values should be 2 but is " + Str( v.Ubound ) ) or failed
  result = ""
  for index = 0 to v.Ubound
    result = result + v( index )
  next index
  failed = PrintIfNot( result = "432", "Result of Values doesn't match: " + result ) or failed
  
  d.Remove( "E" )
  failed = PrintIfNot( d.Count = 2, "Count should be 2 but is " + Str( d.Count ) ) or failed
  
  dim startTime, endTime as Double
  const kLoops = 10000
  
  'd = new Dictionary_MTC( kLoops * 2  ) // Twice kLoops
  d = new Dictionary_MTC( kLoops * 2 + 1  ) // Odd number
  'd = new Dictionary_MTC
  'd = new Dictionary_MTC( 20011 ) // Prime number
  startTime = microseconds
  for index = 1 to kLoops
    d.Value( Str( index ) ) = index
  next index
  endTime = microseconds
  print "Storage took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  
  dim retrieved as Variant
  startTime = microseconds
  for index = 1 to kLoops
    retrieved = d.Value( Str( index ) )
    if retrieved <> index then
      failed = true
      print "Retrieval failed"
      exit
    end if
  next index
  endTime = microseconds
  print "Retrieval took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  
  print "Collisions: " + Str( d.Collisions )
  print "Most Collisions: " + Str( d.MostCollisions )
  
  if FALSE then
    // Compare to array
    dim keyArr( kLoops ) as String
    dim valueArr( kLoops ) As Integer
    for index =  1 to kLoops
      keyArr( index ) = Str( index )
      valueArr( index ) = index
    next index
    
    startTime = microseconds
    for index = 1 to kLoops
      retrieved = valueArr( keyArr.IndexOf( Str( index ) ) )
      if retrieved <> index then
        print "Array lookup failed?"
        exit
      end if
    next index
    endTime = microseconds
    print "Array lookup took " + format( ( endTime - startTime ) / 1000, "#,0.000") + " milliseconds"
  end if
  
  if not failed then
    print "ALL TESTS HAVE PASSED!"
  end if

This is a great class. Thanks!

Is there a way to make your Dictionary case-SENSITIVE?

You could implement dictionaries indirectly via the XojoScript context object. The dictionary methods would
be implemented as methods within the context object and some type of ref/id would be used to reference the dictionary in the script. The dictionaries would be stored in a property of the context object.

eg:
A method called CreateDictionary in the context object would create a new dictionary, store it in a class property (possibly itself a dictionary) and return a ref/id back to the script.
A method called DictionaryAdd in the context object would accept the ref/id among with the key and value. The method would look up the dictionary in its storage and then add the value to the dictionary.

The XojoScript could then do:
Dim ref as Integer
ref = CreateDictionary
DictionaryAdd(ref, thekey, thevalue)

We have successfully used this method to provide access to FolderItems and XML objects within XojoScripts with very little effort.

I think there’s a bug in this function:

Private Function pBinIndex ( key As Variant ) as Integer return key.Hash mod zBinCount End Function

It’s possible for key.Hash to be a negative index which will cause a subsequent OutOfBounds exception. I think the following change fixes it:

Private Function pBinIndex ( key As Variant ) as Integer return Abs(key.Hash mod zBinCount) End Function

On a related note @Kem Tekinay: Is this a better implementation of a Dictionary for Xojoscript (another implementation of yours)? https://forum.xojo.com/23970-xojoscript-dictionary/0. I see it uses a custom Pair class.

Wow, I guess I should pay attention to my own posts. :slight_smile: As the other is “newer”, and included with XsEdit, I’d go with that, but it may suffer from the same bug.

I’ve tried benchmarking the two different dictionary implementations and I think this one marginally edges out the Pair based one in speed (for what it’s worth). Probably 10-20% faster.