defmodule ByteOrderedFloat do @moduledoc """ ByteOrderedFloat is used to encode and decode float values into big-endian-like, order-preserving binaries. ## Acknowledgements Big thanks to Aravind Ravi-Sulekha for a super helpful article. The description: The most significant bit is the sign bit. As all positive numbers are higher than all negative numbers, we should flip the sign bit so it’s 1 for positive numbers and 0 for negative numbers. For negative numbers, we additionally perform a bitwise NOT of the E and M bits so that larger negative numbers get sorted before smaller ones. Source: https://medium.com/@aravindet/order-preserving-encoding-for-floats-cde09c978629 """ import Bitwise @doc """ Encodes floats into a 8-byte binary that is float-order preserving. ## Examples iex> ByteOrderedFloat.encode(-1.0) {:ok, <<64, 15, 255, 255, 255, 255, 255, 255>>} iex> ByteOrderedFloat.encode(0.0) {:ok, <<128, 0, 0, 0, 0, 0, 0, 0>>} iex> ByteOrderedFloat.encode(1.0) {:ok, <<191, 240, 0, 0, 0, 0, 0, 0>>} iex> ByteOrderedFloat.encode(nil) :error """ @spec encode(any) :: :error | {:ok, <<_::64>>} def encode(f) when is_float(f) do {:ok, encode_float_order_preserved(<>)} end def encode(_) do :error end # zero - all 64 bits are zeros therefore the value is zero. defp encode_float_order_preserved(<<0::64>>) do <<1::1, 0::63>> end # negative - when first bit is 1 defp encode_float_order_preserved( <<1::1, exp::big-unsigned-integer-size(11), frac::big-unsigned-integer-size(52)>> ) do e_exp = bnot(exp) e_frac = bnot(frac) <<0::1, e_exp::big-signed-integer-size(11), e_frac::big-signed-integer-size(52)>> end # positive - when first bit is 0 defp encode_float_order_preserved(<<0::1, rest::63>>) do <<1::1, rest::63>> end @doc """ Decodes an ByteOrderedFloat encoded binary into a float. ## Examples iex> {:ok, e} = ByteOrderedFloat.encode(-1.0) iex> ByteOrderedFloat.decode(e) {:ok, -1.0} iex> {:ok, e} = ByteOrderedFloat.encode(0.0) iex> ByteOrderedFloat.decode(e) {:ok, 0.0} iex> {:ok, e} = ByteOrderedFloat.encode(1.0) iex> ByteOrderedFloat.decode(e) {:ok, 1.0} iex> ByteOrderedFloat.decode("1.0") :error """ @spec decode(any) :: :error | {:ok, float} def decode(encoded_float) def decode(<<0::64>>) do {:ok, 0.0} end def decode(<<1::1, rest::63>>) do <> = <<0::1, rest::63>> {:ok, f} rescue MatchError -> :error end def decode(<<0::1, e_exp::big-signed-integer-size(11), e_frac::big-signed-integer-size(52)>>) do exp = bnot(e_exp) frac = bnot(e_frac) <> = <<1::1, exp::big-unsigned-integer-size(11), frac::big-unsigned-integer-size(52)>> {:ok, f} rescue MatchError -> :error end def decode(_) do :error end end