Change value of map array field inside an array of maps

Hi.
I don’t know how i can manipulate a field of type string array inside an array of maps.
The structure is

    [%Category{__meta__: #Ecto.Schema.Metadata<:loaded, "categories">,
    created_by: #Ecto.Association.NotLoaded<association :created_by is not loaded>,
    created_by_id: nil, deleted: false,
    id: "567b4354-1518-4aae-a560-1b9692bce2bd",
    inserted_at: ~N[2018-02-22 20:08:10.774869], main_category: true,
    title: "Lorem ipsum dolor sit amet",
    updated_at: ~N[2018-02-22 20:08:10.774879], used_by: []}]

Now I need to change the field used_by. What I need is to extract the id of the category, which I use in a query to get a count. If count > 0 i want to add a string like “news” or “events” or both to used by.
The schema for this filed is
field :used_by, {:array, :string}, virtual: true, default: []

The function to get the strings is

def set_used_by(categories) do
    query =
      from n in News,
        where: n.category_id == ^x.id,
        select: count(n.id)
    newsCount = Repo.all(query)

    query =
      from e in Events,
        where: e.category_id == ^x.id,
        select: count(e.id)
    eventsCount = Repo.all(query)
  end

The x is from Enum.each … fn(x) which was not working. The function is not finished yet.
Any ideas?

Conceptually all you need to do is something like:

defmodule Demo do

  def set_used_by(category) do
    # determine desired used_by values based on id
    IO.puts "setting #{category.id}"

    # use fake values here
    %{ category | used_by: ["news","event"]}
  end

  def run(list) do
    Enum.map(list, &set_used_by/1)
  end

end

list = [%{created_by: nil,
          created_by_id: nil,
          deleted: false,
          id: "567b4354-1518-4aae-a560-1b9692bce2bd",
          inserted_at: ~N[2018-02-22 20:08:10.774869],
          main_category: true,
          title: "Lorem ipsum dolor sit amet",
          updated_at: ~N[2018-02-22 20:08:10.774879],
          used_by: []}
       ]

IO.inspect(Demo.run(list))

Alternately you could do something like this:

defmodule Demo do

  def gen_used_by(id) do
    # determine desired used_by values based on id
    # use fake values here
    {id, ["news","events"]}
  end

  def merge({{_id, used_by}, category}) do
    Map.put(category, :used_by, used_by)
  end

  def run(list) do
    stream = list
    |> Stream.map(&(Map.get(&1,:id)))
    |> Stream.map(&gen_used_by/1)
    |> Stream.zip(list)
    |> Stream.map(&merge/1)

    Enum.to_list(stream)
  end

end

list = [%{created_by: nil,
          created_by_id: nil,
          deleted: false,
          id: "567b4354-1518-4aae-a560-1b9692bce2bd",
          inserted_at: ~N[2018-02-22 20:08:10.774869],
          main_category: true,
          title: "Lorem ipsum dolor sit amet",
          updated_at: ~N[2018-02-22 20:08:10.774879],
          used_by: []}
       ]

IO.inspect(Demo.run(list))

that way gen_used_by doesn’t have to know anything about the Category struct - that responsibility is shifted to merge/1.

1 Like

Thanks a lot :slight_smile: Will try to understand both approaches and to adept them in my code.
Seems the second one will take a bit longer :wink: Switched from c# to elixir, so still fighting with the deeper understanding.

The core of the issue is that you can’t mutate anything - you have to create a new copy of the structure with the modifications. So each doesn’t cut it - you have to use map.

2 Likes

Yes, in a loop changing values never worked. Thought mayby in elixir. My problem is still the functional programming. In other languages I could have done this in a few minutes. But elixir is still too new to me. Remeber switching from procedural programming to oop. But this switch still needs to arive my brain. Your code works. Need to put some IO.inspect to it, to understand it better. I thought, I need to create a new object I have to return, but thought something like reduce could make it.

Thousand thanks again.