Need help with :ets.match - I always end up with an empty list []

I’m writing a module to test ets match.
It would to store client consume by month/year.

I had try to follow match spec… I even tried to do automagic translate of elixir functions with GitHub - ericmj/ex2ms: :ets.fun2ms for Elixir, translate functions to match specifications

nonetheless I always end up with an empty list []

Please… someone help me with list_period method…

defmodule ETS.Consume do
  @moduledoc """
  Consume tasks

    r ETS.Consume

    ETS.Consume.scenario()

    table = ETS.Consume.start()
    ETS.Consume.populate(table)
    ETS.Consume.list(table, "12345678901")
    ETS.Consume.list_period(table, "12345678901", 2, 2021, 10, 2021)
  """

  import Ex2ms

  @table :cons
  @opts [:ordered_set, :protected]
  def start do
    :ets.new(@table, @opts)
  end

  def insert(ref, cpf_cnpj, mes, ano, consumo) do
    key = {cpf_cnpj, mes, ano}

    :ets.insert(ref, {key, consumo})
  end

  def list(table, cpf_cnpj) do
    :ets.match(table, {{cpf_cnpj, :"$1", :"$2"}, :"$3"})
  end

  def list_period(table, cpf_cnpj, mes_inicio, ano_inicio, mes_fim, ano_fim) do
    pattern = [{{{:"$1", :"$2", :"$3"}, :"$4"}, [{:<, :"$2", 2}], [true]}]

    :ets.match(table, pattern)
  end

  def populate(table) do
    insert(table,  "12345678901", 1, 2021, 100)
    insert(table, "12345678901", 2, 2021, 200)
    insert(table, "12345678901", 3, 2021, 300)
    insert(table, "12345678901", 4, 2021, 200)
    insert(table, "12345678901", 5, 2021, 300)
    insert(table, "12345678901", 6, 2021, 350)
    insert(table, "12345678901", 7, 2021, 200)
    insert(table, "12345678901", 8, 2021, 100)
    insert(table, "12345678901", 9, 2021, 500)
    insert(table, "12345678901", 10, 2021, 300)
    insert(table, "12345678901", 11, 2021, 200)
    insert(table, "12345678901", 12, 2021, 310)
    insert(table, "12345678902", 1, 2021, 310)
  end

  def scenario do
    table = ETS.Consume.start()
    ETS.Consume.populate(table)
    # ETS.Consume.list(table, "12345678901")
    ETS.Consume.list_period(table, "12345678901", 2, 2021, 10, 2021)
  end

  def test() do
    fun do {{ cpf_cnpj, mes, ano }, consumo} = arg when mes < 2 -> true end
  end
end

My workaround will be match by unique key and manually filter…

But this is clearly naive.

:ets.fun2ms(fn {{ cpf_cnpj, mes, ano }, consumo} when mes < 2 -> ano end)

returns

[{{{:"$1", :"$2", :"$3"}, :"$4"}, [{:<, :"$2", 2}], [:"$3"]}]

even this returns an empty list. This is annoying me… if only there is something I can do to know whats wrong, but this is it, even the official mechanisms are giving unexpected results.

Some gentle and helpful soul, please enlighten me?

My next steps would be:

  1. validate erlang itself working.
  2. check elixir and erlang integration spot.

erl

1> T = ets:new(t,[ordered_set]).
#Ref<0.804086672.212992002.220352>
2> ets:insert(T,  {{"12345678901", 1, 2021}, 100}).
true
3> ets:insert(T, {{"12345678901", 2, 2021}, 200}).
true
4> ets:insert(T, {{"12345678901", 3, 2021}, 300}).
true
5> ets:insert(T, {{"12345678901", 4, 2021}, 200}).
true
6> ets:insert(T, {{"12345678901", 5, 2021}, 300}).
true
7> ets:insert(T, {{"12345678901", 6, 2021}, 350}).
true
8> ets:insert(T, {{"12345678901", 7, 2021}, 200}).
true
9> ets:insert(T, {{"12345678901", 8, 2021}, 100}).
true
10> ets:insert(T, {{"12345678901", 9, 2021}, 500}).
true
11> ets:insert(T, {{"12345678901", 10, 2021}, 300}).
true
12> ets:insert(T, {{"12345678901", 11, 2021}, 200}).
true
13> ets:insert(T, {{"12345678901", 12, 2021}, 310}).
true
14> ets:insert(T, {{"12345678902", 1, 2021}, 310}).
true
15> ets:tab2list(T).
[{{"12345678901",1,2021},100},
 {{"12345678901",2,2021},200},
 {{"12345678901",3,2021},300},
 {{"12345678901",4,2021},200},
 {{"12345678901",5,2021},300},
 {{"12345678901",6,2021},350},
 {{"12345678901",7,2021},200},
 {{"12345678901",8,2021},100},
 {{"12345678901",9,2021},500},
 {{"12345678901",10,2021},300},
 {{"12345678901",11,2021},200},
 {{"12345678901",12,2021},310},
 {{"12345678902",1,2021},310}]

And so…

17> ets:match(T, [{{{'$1', '$2', '$3'}, '$4'}, [{'<', '$2', 2}], ['$3']}]).
[]

### [ETS] Does :ets.match spec really works?

Following the official docs: Erlang -- Match Specifications in Erlang

1> T = ets:new(t,[ordered_set]).
#Ref<0.1704239209.3973709829.71159>
2> ets:insert(T, {strider, 1, 2021}).                                        
true
3> ets:match(T, [{{strider,'_','_'}, [], ['$_']}]).
[]

:smile: 5 hours of my life… to uncover that I will need to do Enum.filter :eyes:

There’s an unfortunate overlap in terminology: :ets.match takes a “match pattern”, not a match specification. The documentation for :ets.select/2 demonstrates this:

ets:match(Table,{'$1','$2','$3'})

# is equivalent to

ets:select(Table,[{{'$1','$2','$3'},[],['$$']}])

Demo:

iex(1)> tab = :ets.new(:foo, [:ordered_set])
#Reference<0.3893230368.2635726851.91982>

iex(3)> :ets.insert(tab, {{"a", 1, "b"}, "c"})
true

iex(4)> :ets.insert(tab, {{"d", 2, "e"}, "f"})
true

iex(6)> :ets.select(tab, [{{{:"$1", :"$2", :"$3"}, :"$4"}, [{:<, :"$2", 2}], [:"$_"]}])
[{{"a", 1, "b"}, "c"}]
2 Likes

Thank you so much!!! I’ve been researching the topic for a while.

Thank you thank you thank you!!

It worked :smiley:

(Self promotion) you might find this library to be helpful for you to go either way:

https://hexdocs.pm/match_spec/MatchSpec.html

2 Likes

Hello @ityonemo

whats the advantage over using :ets.fun2ms/1 ?

Complete solution thank to @al2o3cr answer

defmodule ETS.Consume do
  @moduledoc """
  History by key, month, age, value

    r ETS.Consume

    ETS.Consume.scenario()

    table = ETS.Consume.start()
    ETS.Consume.populate(table)
    ETS.Consume.list(table, "12345678901")
    ETS.Consume.list_period(table, "12345678901", 2, 2021, 10, 2021)
  """
  @table :cons
  @opts [:ordered_set, :protected]
  def start do
    :ets.new(@table, @opts)
  end

  def index(age, month) do
    age * 100 + month
  end

  def insert(ref, key, month, age, bytes) do
    index = index(age, month)
    key = {key, index, age, month}

    :ets.insert(ref, {key, bytes})
  end

  def list(table, key) do
    :ets.match(table, {{key, :_, :"$1", :"$2"}, :"$3"})
  end

  def list_period(table, key, begin_month, begin_age, end_month, end_age) do
    begindex = index(begin_age, begin_month)
    endex = index(end_age, end_month)

    # :ets.fun2ms(
    #   fn {{key, index, month, age}, bytes} = _entry when
    #     key == "12345678901" and
    #     index >= 202301 and index <= 202312 ->
    #         {month, age, bytes}
    # end)
    match_spec = [
      {{{:"$1", :"$2", :"$3", :"$4"}, :"$5"},
       [
         {:andalso, {:andalso, {:==, :"$1", key}, {:>=, :"$2", begindex}},
          {:"=<", :"$2", endex}}
       ], [{{:"$3", :"$4", :"$5"}}]}
    ]


    :ets.select(table, match_spec)
  end

  def populate(table) do
    insert(table,  "12345678901", 1, 2021, 100)
    insert(table, "12345678901", 2, 2021, 200)
    insert(table, "12345678901", 3, 2021, 300)
    insert(table, "12345678901", 4, 2021, 200)
    insert(table, "12345678901", 5, 2021, 300)
    insert(table, "12345678901", 6, 2021, 350)
    insert(table, "12345678901", 7, 2021, 200)
    insert(table, "12345678901", 8, 2021, 100)
    insert(table, "12345678901", 9, 2021, 500)
    insert(table, "12345678901", 10, 2021, 300)
    insert(table, "12345678901", 11, 2021, 200)
    insert(table, "12345678901", 12, 2021, 310)
    insert(table, "12345678901", 1, 2022, 410)
    insert(table, "12345678902", 1, 2021, 310)
  end

  def scenario do
    table = ETS.Consume.start()
    ETS.Consume.populate(table)
    # ETS.Consume.list(table, "12345678901")  |> dbg()
    ETS.Consume.list_period(table, "12345678901", 1, 2021, 9, 2021)
  end
end

for elixir, fun2ms only works in the shell.

Also, MatchSpec has “ms2fun”, so you can debug in the other direction if it’s not doing what you expect.

2 Likes