blog.src.rip

Easy color hashing with Erlang’s phash2

Discover a pretty cool hashing function in Erlang.

10/27/2023

This is my new favorite erlang function that’s doesn’t have a direct Elixir version: phash2 .

Here’s the Erlang documentation:

https://www.erlang.org/docs/18/man/erlang#phash2-2

erlang:phash2(Term) -> Hash erlang:phash2(Term, Range) -> Hash Types: Term = term() Range = integer() >= 1 1..2^32 Hash = integer() >= 0 0..Range-1 Portable hash function that gives the same hash for the same Erlang term regardless of machine architecture and ERTS version (the BIF was introduced in ERTS 5.2). The function returns a hash value for Term within the range 0..Range-1. The maximum value for Range is 2^32. When without argument Range, a value in the range 0..2^27-1 is returned. This BIF is always to be used for hashing terms. It distributes small integers better than phash/2, and it is faster for bignums and binaries. Notice that the range 0..Range-1 is different from the range of phash/2, which is 1..Range.

Hashing functions are nothing special, but it’s cool that:

  1. You can pass anything to phash2 , it doesn’t have to be a string.
  2. You can constrain the integer output to a range.

So we can for instance hash a struct, or a string:

iex(1)> :erlang.phash2("hello world")
33550279
iex(2)> :erlang.phash2(%{name: "Alexander"})
46087436

And we can do this:

iex(1)> :erlang.phash2("hello", 360)
59
iex(2)> :erlang.phash2("world", 360)
102

We can use the ability to constrain the output to constrain the result to some integer range like a valid RGB or HSL color. Let’s try that!

defmodule ColorHash do
  def hash(string) do
    # Adding an extra value here just forces each value to be different.
    hue = :erlang.phash2(string <> ":h", 360)
    saturation = :erlang.phash2(string <> ":s", 100)
    lightness = :erlang.phash2(string <> ":l", 100)

    {hue, saturation, lightness}
  end
end

We could feed these colors into like, chat bubbles on a website to color them based on user names. But they’re a little ugly if you just use every saturation value. Here’s a little tweak on the above that lets you constrain the values to something a little more sensible:

defmodule ColorHash do
  @hue_range {0, 360}
  @saturation_range {50, 80}
  @lightness_range {85, 100}

  def hash(string) do
    {min_h, max_h} = @hue_range
    {min_s, max_s} = @saturation_range
    {min_l, max_l} = @lightness_range

    # Adding an extra value here just forces each value to be different.
    hue = :erlang.phash2(string <> ":h", max_h - min_h) + min_h
    saturation = :erlang.phash2(string <> ":s", max_s - min_s) + min_s
    lightness = :erlang.phash2(string <> ":l", max_l - min_l) + min_l

    {hue, saturation, lightness}
  end

  # Expected CSS format.
  def hsl_to_string({h, s, l}) do
    "hsl(#{h} #{s}% #{l}%)"
  end
end

Again, a great option for adding a little unique color to an app for each user.