How to Control Map Key Order in Elixir for Custom Report Formatting?

Elixir is sorting map by key name in ascending order, but I don’t want this behaviour. It is causing me to have undesired reports output. I’d like to have the employee number and names to begin, then followed by leave types instead of the following.

Is there a way to rearrange the map by key in Elixir, so that

records = [
  %{
    "Annual Leave" => "0",
    "Compassionate" => "0",
    "Maternity" => "0",
    "Paternity" => "0",
    "Paternity Leave" => "0",
    "Sick Leave" => 0,
    "Study Leave" => 0,
    "emp_no" => "EMP0004",
    "names" => "Benard Kipucho"
  },
  %{
    "Annual Leave" => "87",
    "Compassionate" => 0,
    "Maternity" => 0,
    "Paternity" => 0,
    "Paternity Leave" => "1024",
    "Sick Leave" => "100",
    "Study Leave" => "989",
    "emp_no" => "EMP001",
    "names" => "Kamaro Lambert"
  }
]

# SOMETHING LIKE THIS CAN make the map starts with emp_no and names.
Enum.map_key_starts_with(records, ["emp_no", "names"])

The above should give the following results

[
  %{
    
    "emp_no" => "EMP0004", # Shifted to the starting point
    "names" => "Benard Kipucho", # Shifted to the starting point
    "Annual Leave" => "0",
    "Compassionate" => "0",
    "Maternity" => "0",
    "Paternity" => "0",
    "Paternity Leave" => "0",
    "Sick Leave" => 0,
    "Study Leave" => 0
  },
  %{
    "emp_no" => "EMP001", # Shifted to the starting point
    "names" => "Kamaro Lambert",# Shifted to the starting point
    "Annual Leave" => "87",
    "Compassionate" => 0,
    "Maternity" => 0,
    "Paternity" => 0,
    "Paternity Leave" => "1024",
    "Sick Leave" => "100",
    "Study Leave" => "989"
  }
]

If you need sorted keys then a Map is the wrong data structure. There are lists, keyword lists, records, custom data structures, etc.

Search for “map with ordered keys” as this has been discussed on here a few times.

3 Likes

Have you considered using structs with a custom inspection protocol implementation?

Maps with string keys deep into the program always smell.

4 Likes

Maps are an unordered dataformat. The BEAM just has a default sort order given it has a global ordering between any term it can represent, which is used to order things where they need to become ordered. So the solution here is to use a datatype which does retain order (lists) instead of maps.

For your usecase I’d even argue that this is a pure view layer concern – as in how data is presented – so your view layer code could simply order the columns as needed before rendering the data.

5 Likes

I haven’t tried this. It looks like I just need to use lists.

It’s not entirely a view issue. I have a pivot table module I pass any list of map and specify what should be rows, colum to transpose and colum to use as value for sum, count etc… then I render it as html, csv, pdf etc… to download or render in a browser.

I will try with lists and see how it works. So far it looks like a list of tuples maintains the order.

Sounds like presentation layer…

3 Likes

I always seem to struggle with choosing the right data structure for modeling data. My (probably naive) way of thinking about it that maps are for data that will need fast insert, fast retrieval, and fast updates but for which order is not (as) important. Lists (including keyword lists) are for things that need fast appends, LIFO order is important, random access does not need to be fast. Key-value pairs that need ordering but also reasonably fast updates, inserts, and lookups I tend to reach for a gb_tree.

Also, you can implement a custom sort function and run your map through as seen in this thread Enum.sort_by to sort a record set (i.e. a list of maps) based on 2 "columns" - #2 by benwilson512

1 Like

Historical trivia: back before maps were introduced into Erlang, Elixir had a module named ListDict which managed the data structure you describe – an ordered list of two element tuples that could be treated like a map.

2 Likes

To make matters worse (? :grinning:) the ordering of elements in maps is completely undefined and changes depending on the number of elements. It use to be that for less than 32 elements the ordering was term ordering and for more elements it was “random” depending on the internal search algorithm used.

The thing to remember is that the actual ordering is purely internal. There are iterator functions which can give you some control of which order you want to step over the elements.

9 Likes

Convert to a list of tuples, then sort using a comparator function

map
  |> Map.to_list()
  |>  |> Enum.sort_by(fn {key, _value} -> key end)

Thank you all for jumping on this issue. I truly appreciate.

I had to write a Report module that:

  1. Aceepts a list of records, and a list of keys in order I want.
  2. Loops throuch each record and map it with Map.get(record, ordered_key, "")
 records = [
     %{
     "Annual Leave" => Decimal.new("21"),
     "Compassionate" => Decimal.new("14"),
     "emp_no" => "EMP001",
     "names" => "Jane Doe"
     },
     %{
     "Annual Leave" => Decimal.new("10"),
     "Compassionate" => Decimal.new("5"),
     "emp_no" => "EMP002",
     "names" => "John Smith"
     }
]
Report.format(records, ["names", "emp_no", "Annual Leave", "Compassionate"])

Gives

 [
     %{
     "names" => "Jane Doe",
     "emp_no" => "EMP001",
     "Annual Leave" => 21,
     "Compassionate" => 14,

     },
     %{
     "names" => "John Smith",
     "emp_no" => "EMP002",
     "Annual Leave" => 10,
     "Compassionate" => 5,
     }
]

What is the definition of Report.format/2? Since your example shows a list of maps, I think you cannot depend on the order of the keys within those maps being consistently applied. Maps are always inherently unordered.

1 Like

Sorry, late to the party. @kamaroly have you come across Explorer? It’s the dataframe library for Elixir (dataframe = in-memory table, roughly). It’s designed for working with small-to-medium datasets.

It may be too heavy weight for what you’re doing. But if you’re finding that you’re reinventing the wheel a lot (e.g. making a custom data structure to implement column order), it may suit your needs.

Also of potential use: the Table package.

2 Likes

Yeah, Report.format/2 should return as list of lists, not a list of maps.

1 Like