最近 A/B テストのように確率的な条件分岐を実現する文脈で、適当な入力文字列から区間 $ [0, 1] $ に含まれる数値を出力するようなハッシュ関数を実装する機会がありました。単に定められた範囲の数値を出力するだけでなく、出力される数値に偏りが出ないように(関数の出力値の分布が連続一様分布とみなせる 1 ように)するという要件もありました。
イメージとしては以下のコードのようになります。
// 同じ文字列を入力に取ると同じ数値が出力される
strToNumberBetweenZeroAndOne('abc'); // output: 0.7283949105904
strToNumberBetweenZeroAndOne('abc'); // output: 0.7283949105904
// 別の文字列を入力に取ると別の数値が出力される
strToNumberBetweenZeroAndOne('Hello'); // output: 0.095208030923864
strToNumberBetweenZeroAndOne('World!'); // output: 0.31755707966684
strToNumberBetweenZeroAndOne('0123456789'); // output: 0.51892998626938
// 色々な文字列に対して出力値をプロットしていくと、数値の確率分布が区間 [0, 1] の一様分布に近づく。
このような関数を PHP と TypeScript で実装したので紹介します。
実装
PHP
PHP の場合は以下のように実装できます。
function strToNumberBetweenZeroAndOne(string $message): float
{
// 1
$hashHex = hash('sha256', $message);
// 2
$maxHex = str_repeat("f", 64);
return hexdec($hashHex) / hexdec($maxHex);
}
1 の部分では hash('sha256', $string)
で入力文字列を SHA256 2 で64桁の固定長16進数に変換し、 hexdec()
でその10進数表現を得ています。文字列から16進数への変換は bin2hex で行うこともできますが、文字列のサイズが大きすぎると桁あふれを起こしてしまうので使用を避けています。
2 の部分では 1 で得られた10進数を64桁の16進数の最大値 str_repeat("f", 64)
の10進数表現で割ることで数値を算出しています。
TypeScript
import CryptoJS from 'crypto-js'
const strToNumberBetweenZeroAndOne = (message: string): number => {
const hashHex = CryptoJS.SHA256(message).toString()
const maxHex = 'f'.repeat(64)
return parseInt(hashHex, 16) / parseInt(maxHex, 16)
}
内容は PHP と特に変わりませんが、 SHA256 のハッシュ化を行うために crypto-js というライブラリを利用しています。
検証
実際にランダムな文字列を入力にメソッドの出力値 3 を集計してヒストグラムを作成してみると、おおよそ一様分布になっていることが分かります。