Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Bitfields Post

blogging/bitfields.livemd

Bitfields Post

Mix.install([
  {:jason, "~> 1.4"}
])

CandyData

defmodule CandyData do
  @priority ~w(low medium high urgent)
  @location 1..100
  @status ["not empty", "empty"]
  @candy [
    "peppermint patties",
    "m&ms",
    "reese's pieces",
    "butterfingers",
    "cookies"
  ]

  def serialize(data) do
    [
      priority(data),
      location(data),
      status(data),
      candy(data)
    ]
  end

  def deserialize([priority, location, status, candy]) do
    room =
      if location in @location do
        "Room #{location}"
      else
        nil
      end

    %{
      "priority" => Enum.at(@priority, priority),
      "location" => room,
      "status" => Enum.at(@status, status),
      "candy" => Enum.at(@candy, candy)
    }
  end

  defp priority(%{"priority" => priority}) do
    Enum.find_index(@priority, &(&1 == priority))
  end

  defp location(%{"location" => "Room " <> room}) do
    room = String.to_integer(room)

    if room in @location do
      room
    else
      :error
    end
  end

  defp status(%{"status" => status}) do
    Enum.find_index(@status, &amp;(&amp;1 == status))
  end

  defp candy(%{"candy" => candy}) do
    Enum.find_index(@candy, &amp;(&amp;1 == candy))
  end
end
data = %{
  "candy" => "peppermint patties",
  "location" => "Room 71",
  "priority" => "urgent",
  "status" => "empty"
}
data
|> CandyData.serialize()
|> CandyData.deserialize()
|> dbg()
other_data = %{
  "priority" => "low",
  "location" => "Room 23",
  "status" => "not empty",
  "candy" => "m&ms"
}
other_data
|> CandyData.serialize()
|> CandyData.deserialize()
|> dbg()
data
|> CandyData.serialize()
|> Enum.map(&amp;Integer.to_string(&amp;1, 2))
|> dbg()
json_base10_numbers = [3, 71, 1, 0] |> Jason.encode!()
json_base2_strings = ["11", "1000111", "1", "0"] |> Jason.encode!()
json_base10_numbers
|> byte_size()
|> dbg()
json_base2_strings
|> byte_size()
|> dbg()
bitfield = 0b1110001111000

[2, 8, 10, 12, 16]
|> Enum.each(fn base ->
  representation = Integer.to_string(bitfield, base)

  case base do
    digit when digit < 10 ->
      IO.puts("Base #{base}:  #{representation}")

    _other ->
      IO.puts("Base #{base}: #{representation}")
  end
end)
["11", "1000111", "1", "0"]
|> Enum.join()
|> String.to_integer(2)
|> dbg()

["11", "1000111", "1", "000"]
|> Enum.join()
|> String.to_integer(2)
|> dbg()
Integer.to_string(1822, 2) |> dbg()
Integer.to_string(7288, 2) |> dbg()
1822
|> Integer.to_string(2)
|> String.pad_trailing(13, "0")
|> String.to_integer(2)
|> dbg()

7288
|> Integer.to_string(2)
|> String.pad_trailing(13, "0")
|> String.to_integer(2)
|> dbg()

BitfieldGenerator

class BitfieldGenerator
  attr_reader :fields

  def initialize
    @fields = []
    @_current_exponent = 0
  end

  def add_field(name, bits)
    field_exponent = @_current_exponent
    @_current_exponent += bits
    @fields << {
      name: name,
      bits: bits,
      base: 2 ** field_exponent
    }
  end

  # operations kept distinct to aide readability
  def serialize(data_values)
    calculated = field_values(data_values)
    # => [0, 8, 1136, 6144]

    calculated.sum
    # => 7288
  end

  def field_values(values)
    values.map.with_index do |value, index|
      value * @fields[index][:base]
    end
  end
end

And using that code

bf = BitfieldGenerator.new
bf.add_field('candy', 3)
bf.add_field('status', 1)
bf.add_field('location', 7)
bf.add_field('priority', 2)

Results in

[
  {
    :name=>"candy",
    :bits=>3,
    :base=>1
  },
  {
    :name=>"status",
    :bits=>1,
    :base=>8
  },
  {
    :name=>"location",
    :bits=>7,
    :base=>16
  },
  {
    :name=>"priority",
    :bits=>2,
    :base=>2048
  }
]
defmodule Bitfield do
  defstruct fields: [], exponent: 0

  defmodule Field do
    defstruct [:name, :length, :units]
  end

  def new() do
    %__MODULE__{}
  end

  def add_field(%__MODULE__{} = bitfield, name, bits) do
    bitfield
    |> update_in(
      [Access.key!(:fields)],
      &amp;(&amp;1 ++ [%Field{name: name, length: bits, units: 2 ** bitfield.exponent}])
    )
    |> put_in([Access.key!(:exponent)], bitfield.exponent + bits)
  end

  def to_integer(%__MODULE__{} = bf, values)
      when is_map(values) and map_size(values) == length(bf.fields) do
    Enum.reduce(bf.fields, 0, fn field, acc ->
      acc + field.units * Map.get(values, field.name)
    end)
  end

  def to_integer(_bf, _values), do: {:error, :invalid}
end
bf =
  Bitfield.new()
  |> Bitfield.add_field(:candy, 3)
  |> Bitfield.add_field(:status, 1)
  |> Bitfield.add_field(:location, 7)
  |> Bitfield.add_field(:priority, 2)
Bitfield.to_integer(bf, %{candy: 0, status: 1, location: 71, priority: 3})

Left Shift

import Bitwise

bitfield = 0

candy_value = 0
candy_bits = 3
candy_position = 0

bitfield = bitfield + (candy_value <<< candy_position)
dbg(bitfield |> Integer.to_string(2))

status_value = 1
status_bits = 1
status_position = candy_position + candy_bits

bitfield = bitfield + (status_value <<< status_position)
dbg(bitfield |> Integer.to_string(2))

location_value = 71
location_bits = 7
location_position = status_position + status_bits

bitfield = bitfield + (location_value <<< location_position)
dbg(bitfield |> Integer.to_string(2))

priority_value = 3
priority_bits = 2
priority_position = location_position + location_bits

bitfield = bitfield + (priority_value <<< priority_position)
dbg(bitfield |> Integer.to_string(2))

Bitwise Right Shift

import Bitwise

for n <- 0..9 do
  result = 55 >>> n
  result_string = String.pad_leading("#{result}", 2, " ")

  result
  |> Integer.to_string(2)
  |> String.pad_leading(6, " ")
  |> IO.inspect(label: "55 >>> #{n} = #{result_string}")
end

Pattern matching bitfields

<> = <<0b00011011>>
dbg(b)
<> = <<27::8>>
dbg(b)
<> = <<7288::13>>
{priority, location, status, candy} |> dbg()

Parsing candy status bitfield

bitfield = 0b1110001111000

candy = 0b0000000000111
candy_shift_right = 0

status = 0b0000000001000
status_shift_right = 3

location = 0b0011111110000
location_shift_right = 4

priority = 0b11
priority_shift_right = 11

bitfield
|> Bitwise.band(status)
|> Bitwise.bsr(status_shift_right)
|> dbg()

bitfield
|> Bitwise.band(location)
|> Bitwise.bsr(location_shift_right)
|> dbg()

In and Out Again

defmodule Bitfield.V2 do
  defstruct fields: [], exponent: 0

  defmodule Field do
    defstruct [:name, :length, :position, :units]
  end

  def new() do
    %__MODULE__{}
  end

  def add_field(%__MODULE__{} = bitfield, name, bits) do
    bitfield
    |> update_in(
      [Access.key!(:fields)],
      &amp;(&amp;1 ++
          [
            %Field{
              name: name,
              length: bits,
              units: 2 ** bitfield.exponent
            }
          ])
    )
    |> put_in([Access.key!(:exponent)], bitfield.exponent + bits)
  end

  def to_integer(%__MODULE__{} = bf, values)
      when is_map(values) and map_size(values) == length(bf.fields) do
    Enum.reduce(bf.fields, 0, fn field, acc ->
      acc + field.units * Map.get(values, field.name)
    end)
  end

  def to_integer(_bf, _values), do: {:error, :invalid}

  def to_fields(%__MODULE__{} = bf, value) do
    bf.fields
    |> Enum.reverse()
    |> Enum.reduce({[], value, bf.exponent}, fn field, {results, val, length} ->
      <> = <>
      results = [%{field.name => result} | results]
      {results, rest, length - field.length}
    end)
    |> then(fn {results, _val, _length} ->
      results
    end)
  end
end
bf =
  Bitfield.V2.new()
  |> Bitfield.V2.add_field(:candy, 3)
  |> Bitfield.V2.add_field(:status, 1)
  |> Bitfield.V2.add_field(:location, 7)
  |> Bitfield.V2.add_field(:priority, 2)
value = Bitfield.V2.to_integer(bf, %{candy: 0, status: 1, location: 71, priority: 3})
fields = Bitfield.V2.to_fields(bf, value)