Enum.sort_by list of maps

I have a list of maps.

post_a =
%{
post_body_size: 4,
date: ~D[2021-06-05]
}
post_b =
%{
post_body_size: 4,
date: ~D[2021-06-04]
}
post_c =
%{
post_body_size: 4,
date: ~D[2021-06-03]
}
post_d =
%{
post_body_size: 5,
date: ~D[2021-06-02]
}

I am trying to organize the posts based on the body size (ascending order) and for cases when body sizes are same, the next sort_by is the date(descending order).

[post_count_1, post_count_2, post_count_3, post_count_4] =
        [post_a, post_b, post_c, post_d]
        |> Enum.sort_by(&{Map.fetch(&1, :post_body_size), {:desc, Map.fetch(&1, :date)}})

This doesn’t seem to sort the posts with (only) same body size according to date. Any inputs on how to resolve this or where I am possibly going wrong?

In order to compare Date structs, you have to pass the Date module as the third argument to Enum.sort_by/3 as shown in the very last example from Enum - sort_by/3. But by doing this I’m not sure if you can do the Integer comparison for :post_body_size in the same function. Here’s a two-pass implementation:

post_list
|> Enum.sort_by(&Map.fetch!(&1, :date), {:desc, Date})
|> Enum.sort_by(&Map.fetch!(&1, :post_body_size))
1 Like

Thank you so much This worked for when I want to sort the post by post_body_size ascending and date desc. I also have another condition where I want to order post_body_size by descending where the integer comparison doesn’t work

You can sort :post_body_size like

...
|> Enum.sort_by(&Map.fetch!(&1, :post_body_size), :desc)

and it will be stable because :desc is a convenience for >=/2

I tried this but it doesn’t order correctly

What exactly are the sort conditions?

I have four posts and I want to able to order it according to both asc and desc post_body_size. The default date order is desc so if the post_body_sizes are same, it defaults to order according to date desc(only the posts that have same post_body_size). I need the top two posts in both the cases.

This is for test.

I’m still having a little trouble understanding. Can you show the test code you’ve already written?

I am sorry, its working, I have been trying to assert with the wrong posts. Your solution works for both the cases. Thanks a lot!

1 Like

Can this be done in a single line? Like

|> Enum.sort_by(& {&1.date, {:desc, Date}, &1.post_body_size, :desc})

This is a wrong syntax but is there a right way to do it?

Looking at the source for Enum.sort_by/3, I didn’t see any way to do it, because the function expects three arguments:

  • the Enumerable to be sorted
  • the mapper, which retrieves the desired elements
  • the sorter, which compares the elements.

You are able to “overload” the mapper with a tuple of elements, because the sorter will apply to each element in the tuple one-by-one. But the same sorter applies to all elements in the tuple. So in your case, where you want to use two different sorters (>=/2 for Integers, and Date.compare/2 for Dates), I think you must call the function twice.

Thank you for the explanation. Is it possible to explain with an example? " sorter will apply to each element in the tuple one-by-one. But the same sorter applies to all elements in the tuple." I think I am not able to understand this part.

Let’s say the mapper returns two elements in a tuple:

&{Map.fetch!(&1, :post_body_size), Map.fetch!(&1, :date)}

like

{4, ~D[2021-06-05]}
{4, ~D[2021-06-04]}
{5, ~D[2021-06-02]}
etc...

Now if the sorter is &>=/2 (or :desc) then it will compare

4 >= 4
4 >= 5
etc...

But it will also use the same sorter &>=/2 to compare the second elements in the tuple:

~D[2021-06-05] >= ~D[2021-06-04]
~D[2021-06-04] >= ~D[2021-06-02]

And this does not work because you cannot use >=/2 to sort Structs (see Enum.sort/2 - Sorting structs). Instead, you must use the Date.compare function as the sorter by passing the Date module as the third argument:

post_list
|> Enum.sort_by(&{Map.fetch!(&1, :post_body_size), Map.fetch!(&1, :date)}, Date)

This is ok:

Date.compare(~D[2021-06-05], ~D[2021-06-04])
Date.compare(~D[2021-06-04], ~D[2021-06-02])

But now we have the opposite problem when sorting by :post_body_size:

Date.compare(4, 4)
Date.compare(4, 5)

This will not work because Date.compare expects %Date{} structs, not Integers.

Makes sense. Can we not add a tuple of sorters? We do something like this here Enum.sort_by(&Map.fetch!(&1, :post_body_size), :desc) so I tried {{:desc, Date}, :desc} but this didn’t work. Is it because of the arity issue? elixir/enum.ex at d7f96d833235fa471a58d3cfc22c01ca38e418bf · elixir-lang/elixir · GitHub

No. The possible options are:

(element(), element() -> boolean())
  | :asc
  | :desc
  | module()
  | {:asc | :desc, module()}
)

Got it, thank you so much. I got a real good understanding of this concept.

1 Like