Convert list into date based map

I have read a lot of posts about mapping, but I can’t help with this.

I have database table of currencies with conversion rates for each day:

   schema "fxrates" do
     field :from, :string
     field :of_date, :date
     field :to, :string
     field :value, :decimal
   end

and I want to create a json with format

{
    "for-date": "2025-02-14",
    "rates": {
        "2025-02-01":  [  
           {"from": "US$", "to": "EUR", "value": 0.9},
           {"from": "EUR", "to": "US$", "value": 1.2}
         ],
       "2025-02-01":  [  
           {"from": "US$", "to": "EUR", "value": 0.9},
           {"from": "EUR", "to": "US$", "value": 1.2}
       ],
.. .
     }
}

What I’ve found so far is

    fxrates = Currencies.list_fxrates()
    |> Enum.group_by(fn x -> x.of_date end)
    |> Enum.map(fn {x,y} -> {x, Dict.values y} end)

but the mapping does not work at all. I think I lack an understanding of how grouping works and how to convert it.

:wave: @Slesa

Could this be what you are looking for?

fxrates =
  [
    %{from: "US$", to: "EUR", value: Decimal.new("0.9"), of_date: ~D[2025-02-01]},
    %{from: "EUR", to: "US$", value: Decimal.new("1.2"), of_date: ~D[2025-02-01]},
    %{from: "US$", to: "EUR", value: Decimal.new("0.9"), of_date: ~D[2025-02-01]},
    %{from: "EUR", to: "US$", value: Decimal.new("1.2"), of_date: ~D[2025-02-01]}
  ]

rates =
  fxrates
  |> Enum.group_by(& &1.of_date)
  |> Map.new(fn {date, rates} ->
    {Date.to_string(date), Enum.map(rates, fn rate -> Map.take(rate, [:from, :to, :value]) end)}
  end)

JSON.encode!(%{
  "for-date" => "2025-02-14",
  "rates" => rates
})

It produces the following JSON:

{
  "for-date": "2025-02-14",
  "rates": {
    "2025-02-01": [
      {
        "value": "0.9",
        "from": "US$",
        "to": "EUR"
      },
      {
        "value": "1.2",
        "from": "EUR",
        "to": "US$"
      },
      {
        "value": "0.9",
        "from": "US$",
        "to": "EUR"
      },
      {
        "value": "1.2",
        "from": "EUR",
        "to": "US$"
      }
    ]
  }
}
2 Likes

Yes, exactly! Thanks a lot!

So the grouping creates already the list of rates.

You might want to use IO.inspect or dbg in your code so you can see intermediate results. Helps a lot with iterating on code.

2 Likes

Final solution so far (slight change in the answer syntax):

  defmodule FxRateReply do
    @derive Jason.Encoder
    defstruct from_currency_code: :string, to_currency_code: :string, value: :decimal

    defimpl Jason.Encoder do #, for: FxRateEntry do
      @impl Jason.Encoder
      def encode(value, opts) do
        Jason.Encode.map(%{
          fromCurrencyCode: Map.get(value, :from_currency_code),
          toCurrencyCode: Map.get(value, :to_currency_code),
          value: Map.get(value, :value, 1.0) |> Decimal.to_string()
        }, opts)
      end
    end
  end

  def all_rates(conn, _params) do
    fxrates = Currencies.list_fxrates()
      |> Enum.group_by(fn x -> x.of_date end)
      |> Enum.map(fn {date, rates} -> %{
            asOfDate: Date.to_string(date),
            fxRateSource: "Elixir source",
            rates: Enum.map(rates, fn rate -> %FxRateReply{
                from_currency_code: Map.get(rate, :from),
                to_currency_code: Map.get(rate, :to),
                value: Map.get(rate, :value)
              } end)
            } end)
    json(conn, fxrates)
  end

Probably I can throw away the Jason encoder and write it directly as mentioned above.

And now I am trying to get only requested dates/currency combinations…