Excel VBAを使用して長い文字列の短いハッシュを取得する方法
与えられたもの
これまでに行ったこと
私は this SO answer は4桁の16進コード(CRC16)を生成するので、良いスタートだと思いました。
しかし、4桁は少なかった。 400文字列のテストでは、20%がどこか他の場所で重複しています。
衝突が発生する可能性が高すぎます。
Sub tester()
For i = 2 To 433
Cells(i, 2) = CRC16(Cells(i, 1))
Next i
End Sub
Function CRC16(txt As String)
Dim x As Long
Dim mask, i, j, nC, Crc As Integer
Dim c As String
Crc = &HFFFF
For nC = 1 To Len(txt)
j = Val("&H" + Mid(txt, nC, 2))
Crc = Crc Xor j
For j = 1 To 8
mask = 0
If Crc / 2 <> Int(Crc / 2) Then mask = &HA001
Crc = Int(Crc / 2) And &H7FFF: Crc = Crc Xor mask
Next j
Next nC
CRC16 = Hex$(Crc)
End Function
再現方法
これらの400 Pastebinからのテスト文字列 をコピーできます。
新しいExcelブックの列Aに貼り付けて、上記のコードを実行します。
Q:短いパーセンテージの重複を取得するのに十分な長さ(12文字)と十分な長さの文字列ハッシュを取得するにはどうすればよいですか。
文字列を3つの短い文字列に分割します(3で割り切れない場合、最後の文字列は他の2つの文字列よりも長くなります)。それぞれに対して「短い」アルゴリズムを実行し、結果を連結します。
私はコードを書くことができましたが、質問の質に基づいて、ここから取得できると思います!
編集:そのアドバイスでは不十分であることが判明しました。元のCRC16コードに重大な欠陥があります-つまり、次の行です。
_j = Val("&H" + Mid(txt, nC, 2))
_
これは16進値として解釈できるテキストのみを処理します。小文字と大文字は同じであり、アルファベットのFの後はすべて無視されます(私が知る限り)。何か良いことが出てくるというのは奇跡です。行を次のように置き換えた場合
_j = asc(mid(txt, nC, 1))
_
物事はうまく機能します-すべてのASCIIコードは、少なくとも独自の値として人生を始めます。
この変更を以前に作成した提案と組み合わせると、次のコードが得られます。
_Function hash12(s As String)
' create a 12 character hash from string s
Dim l As Integer, l3 As Integer
Dim s1 As String, s2 As String, s3 As String
l = Len(s)
l3 = Int(l / 3)
s1 = Mid(s, 1, l3) ' first part
s2 = Mid(s, l3 + 1, l3) ' middle part
s3 = Mid(s, 2 * l3 + 1) ' the rest of the string...
hash12 = hash4(s1) + hash4(s2) + hash4(s3)
End Function
Function hash4(txt)
' copied from the example
Dim x As Long
Dim mask, i, j, nC, crc As Integer
Dim c As String
crc = &HFFFF
For nC = 1 To Len(txt)
j = Asc(Mid(txt, nC)) ' <<<<<<< new line of code - makes all the difference
' instead of j = Val("&H" + Mid(txt, nC, 2))
crc = crc Xor j
For j = 1 To 8
mask = 0
If crc / 2 <> Int(crc / 2) Then mask = &HA001
crc = Int(crc / 2) And &H7FFF: crc = crc Xor mask
Next j
Next nC
c = Hex$(crc)
' <<<<< new section: make sure returned string is always 4 characters long >>>>>
' pad to always have length 4:
While Len(c) < 4
c = "0" & c
Wend
hash4 = c
End Function
_
このコードは、スプレッドシートに=hash12("A2")
などとして配置できます。おもしろいのは、「新しく改良された」hash4アルゴリズムを使用して、比較する方法を確認することもできます。衝突をカウントするピボットテーブルを作成しました。_hash12
_アルゴリズムには何もなく、_hash4
_には3つしかありませんでした。これから_hash8
_、...を作成する方法を理解できると思います。あなたの質問の「一意である必要はない」は、おそらく「改善された」_hash4
_で十分であることを示唆しています。
原則として、4文字の16進数は64kの一意の値を持つ必要があります。そのため、同じハッシュを持つ2つのランダムな文字列の可能性は64kに1つになります。 400個の文字列がある場合、400 x 399/2の「可能性のある衝突ペア」〜8万回の機会があります(非常にランダムな文字列があったと想定)。したがって、サンプルデータセットで3つの衝突を観察することは、不合理なスコアではありません。文字列Nの数が増えると、衝突の確率はNの2乗になります。hash12に32ビットの情報が追加されているため、N> 20 M程度の場合に衝突が発生すると予想されます(ハンドウェービング、インマイヘッド数学)。
明らかにhash12コードをもう少しコンパクトにすることができます-そして、それを任意の長さに拡張する方法を見るのは簡単なはずです。
最後にもう1つ。 RCアドレス指定を有効にしている場合、スプレッドシートの数式として=CRC16("string")
を使用すると、追跡が困難な_#REF
_エラーが発生します...そのため、名前を_hash4
_に変更しました
多分他の人はこれが便利だと思うでしょう。
VBAで文字列の短いハッシュを生成するいくつかの異なる関数を収集しました。
私はコードを信用せず、すべてのソースが参照されています。
=CRC16HASH(A1)
with this Code=CRC16NUMERIC(A1)
with this Code=CRC16TWICE(A1)
with this Code=SHA1TRUNC(A1)
with this Code=BASE64SHA1(A1)
with this Codeここ は、すべてのサンプル関数と多数のテスト文字列を含むテストワークブックです。
独自の機能を自由に追加してください。
衝突のレベルが低い文字列の32ビットハッシュ関数:
Public Function StrHash(text As String) As Long
Dim i As Long
StrHash = &H65D5BAAA
For i = 1 To Len(text)
StrHash = ((StrHash + AscW(Mid$(text, i, 1))) Mod 69208103) * 31&
Next
End Function
または64ビットのハッシュ関数として:
Public Function StrHash64(text As String) As String
Dim i&, h1&, h2&, c&
h1 = &H65D5BAAA
h2 = &H2454A5ED
For i = 1 To Len(text)
c = AscW(Mid$(text, i, 1))
h1 = ((h1 + c) Mod 69208103) * 31&
h2 = ((h2 + c) Mod 65009701) * 33&
Next
StrHash64 = Right("00000000" & Hex(h1), 8) & Right("00000000" & Hex(h2), 8)
End Function
以下はハッシュ関数ではありませんが、小さなリスト(検査で確認できるほど小さい)で衝突率が低い数値IDを簡単に生成する方法として使用しました。
仕組み:列Aは行2以降の文字列を保持します。行1では、A1とB1は文字列の途中の任意の開始位置と終了位置を保持します。この式では、文字列の最初の文字と文字列の中央から取得した固定文字を使用し、衝突の可能性を減らすためにLEN()を「ファンニング関数」として使用しています。
=CODE(A2)*LEN(A2) + CODE(MID(A2,$A$1,$B$1))*LEN(MID(A2,$A$1,$B$1))
固定幅フィールドを持つデータベーステーブルから文字列を取得する場合、長さをトリミングする必要がある場合があります。
=CODE(TRIM(C8))*LEN(TRIM(C8))
+CODE(MID(TRIM(C8),$A$1,1))*LEN(MID(TRIM(C8),$A$1,$B$1))