TypeScript 实现 .NET 字符串HashCode算法

发布于:7/24/2019, 2:46:19 PM @孙博
技术分享 | TypeScript
许可协议:署名-非商业性使用(by-nc)

因为跨部门协作的原因,我们需要为主要使用Nodejs部门的同事提供一个SDK,我们此前已经提供了 .NET、.NET Core、Java、Golang 等多种语言的封装,其中有一个获取字符串HashCode的代码,为了保证各个平台计算结果统一,我们通过反编译方式获取到了 .NET Framework 的实现,并翻译成了其他语言。

.NET Framework 的代码大致如下:

// HashUtils.cs
public sealed class HashUtils
{
    private HashUtils() { }
    /// <summary>
    /// 计算哈希值
    /// </summary>
    /// <param name="text">对象字符串</param>
    /// <returns>哈希结果</returns>
    public static int CalcHashCode(string text)
    {
        var chars = Encoding.Unicode.GetBytes(text);
        var half_pre = 0x15051505;
        var half_post = half_pre;
        var index = 0;
        for (int i = text.Length; i > 0; i -= 4)
        {
            var data = new int[]
            {
                    calc(chars, index + 3, 24) + calc(chars, index + 2, 16) + calc(chars, index + 1, 8) + calc(chars, index + 0, 0),
                    calc(chars, index + 7, 24) + calc(chars, index + 6, 16) + calc(chars, index + 5, 8) + calc(chars, index + 4, 0),
            };
            half_pre = (((half_pre << 5) + half_pre) + (half_pre >> 0x1b)) ^ data[0];
            if (i <= 2)
            {
                break;
            }
            half_post = (((half_post << 5) + half_post) + (half_post >> 0x1b)) ^ data[1];
            index += 8;
        }
        return half_pre + (half_post * 0x5d588b65);
    }

    private static int calc(byte[] bytes, int index, int offset)
    {
        if (index >= bytes.Length)
        {
            return 0;
        }
        return bytes[index] << offset;
    }
}

类似的代码我们也成功的翻译为其他语言,但是在Nodejs遇到这个需求时,我们遇到了麻烦——Nodejs是没有“类型”的概念的,更是没有Int32来将有符号的整型数字控制在4个字节以内,这就导致了在Nodejs下,如果将一个数字乘以0x5d588b65的话,其结果有很大可能会超过32位,于是我们只好另辟蹊径,使用位移的方式模拟乘法运算。其代码类似如下(尚未做优化,仅做了功能实现)。

// hash-utils.ts
export function getHashCode(text: string): number {
  if (!text) {
    return 0
  }
  let buffer = Buffer.from(text, 'utf16le')
  let ints = new Array<number>(buffer.byteLength)
  for (let i = 0; i < ints.length; i++) {
    ints[i] = buffer[i] & 0xff
  }
  let halfPre = 0x15051505
  let halfPost = halfPre
  let index = 0
  for (let i = text.length; i > 0; i -= 4) {
    let data = [
      calc(ints, index + 3, 24) +
        calc(ints, index + 2, 16) +
        calc(ints, index + 1, 8) +
        calc(ints, index + 0, 0),
      calc(ints, index + 7, 24) +
        calc(ints, index + 6, 16) +
        calc(ints, index + 5, 8) +
        calc(ints, index + 4, 0)
    ]
    halfPre = ((halfPre << 5) + halfPre + (halfPre >> 0x1b)) ^ data[0]
    if (i <= 2) {
      break
    }
    halfPost = ((halfPost << 5) + halfPost + (halfPost >> 0x1b)) ^ data[1]
    index += 8
  }
  return int32Add(halfPre, int32Multi0x5d588b65(halfPost))
}

function calc(ints: Array<number>, index: number, offset: number): number {
  if (index >= ints.length) {
    return 0
  }
  return ints[index] << offset
}

function int32Multi0x5d588b65(value: number): number {
  const powers = [1, 3, 6, 7, 9, 10, 12, 16, 20, 21, 23, 25, 27, 28, 29, 31]
  const length = powers.length
  let result = 0

  for (let i = 0; i < length; i++) {
    result = int32Add(result, int32Move(value, powers[i]))
  }

  return result
}

function int32Add(left: number, right: number): number {
  return (left + right) & 0xffffffff
}

function int32Move(value: number, power: number): number {
  if (power === 1) {
    return value
  } else {
    return (value << (power - 1)) & 0xffffffff
  }
}