Ivanari
Ivanari

Reputation: 313

Split a list into N parts

Given a list, how can I split it into N sub-lists? They don't have necessarily have to be of the equal size. For instance, given 9 elements and split it into N = 3 sub-lists => 3x3. Or into N = 4 sub-lists => 2, 2, 2 and 1.

How can I do that? Isn't there a function in Elixir library?

Enum.split splits a list into 2 parts

update:

If I have 7 elements and I want to split them into 3 sub-lists, there should be created 3 sublists:

[[3 elements], [2 elements], [2 elements]] 

Namely, I want to preserve all the elements

Upvotes: 5

Views: 4517

Answers (3)

Lionel Rowe
Lionel Rowe

Reputation: 5926

Found this question, but none of the current answers worked for my use case, for which I wanted the group sizes to be as uniform as possible. For example:

Enum.to_list(1..100) |> chunk_uniformly(6) |> Enum.map(&length/1)

# desired result is [17, 17, 17, 17, 16, 16]
# current accepted answer returns [20, 16, 16, 16, 16, 16]

Here's my eventual solution:

def chunk_uniformly(list, number_of_chunks) do
  len = length(list)

  optimal_chunk_size = div(len + number_of_chunks - 1, number_of_chunks)

  overspill = optimal_chunk_size * number_of_chunks - len

  {full_chunks, partial_chunks} =
    list
    |> Enum.split((number_of_chunks - overspill) * optimal_chunk_size)

  Enum.concat(
    full_chunks |> Enum.chunk_every(optimal_chunk_size),
    case partial_chunks do
      [] -> List.duplicate([], overspill)
      ls -> ls |> Enum.chunk_every(optimal_chunk_size - 1)
    end
  )
end

If, like in my use case, you're actually working from a maximum chunk size, you can get the number of chunks like this:

number_of_chunks = div(length(list) + max_chunk_size - 1, max_chunk_size)

Upvotes: 0

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

Let’s assume we want 3 chunks:

list = [1, 2, 3, 4, 5, 6, 7]
chunk_count = 3
chunk_length =
  list
  |> Enum.count()
  |> div(chunk_count)


list
|> Enum.with_index() # to calculate position
|> Enum.reduce(%{}, fn {e, i}, acc ->
     Map.update(acc, rem(div(i, chunk_length), chunk_count), [e], &([e | &1]))
   end)
|> Map.values()      # since we do need lists
|> Enum.map(&Enum.reverse/1)
#⇒ [[1, 2, 7], [3, 4], [5, 6]]

If lists in Elixir were to implement Access behaviour, the code would be much simpler as we could use Kernel.put_in/3.

Upvotes: 0

Kevin Johnson
Kevin Johnson

Reputation: 1970

You could consider using Enum.chunk_every to split the list into sublists of n amount elements each:

some_list = [1, 2, 3, 4, 5, 6]
Enum.chunk_every(some_list, 2)
[[1, 2], [3, 4], [5, 6]]

By calculating the total length of the list first:

total_length = length(some_list)

and dividing that number with the desired amount of parts giving you the length of each chunk:

desired_amount_of_sublists = 3
chunk_length = Integer.floor_div(total_length, desired_amount_of_sublists)

should allow you to arbitrarily chunk the list into as many parts you require:

Enum.chunk_every(some_list, chunk_length)
[[1, 2], [3, 4], [5, 6]]

In the case that you have a hard requirement to have each sublist be of exactly n elements, then you can pass in the option :discard to discard the last sublist if it is less than n elements:

Enum.chunk_every([1,2,3,4,5,6,7], 2, 2, :discard)
[[1, 2], [3, 4], [5, 6]]

In the case that you have the hard requirement where you cannot discard any element and for instance you need the remainder elements to be incorporated with the first sublist, then you can do as follows:

Say that with the above you arrive at:

result_so_far = Enum.chunk_every([1,2,3,4,5,6,7], 2)
[[1, 2], [3, 4], [5, 6], [7]]

First reverse result_so_far, and take the first sublist thereof, which would be [7], as follows:

[last_sublist | other_sublists] = Enum.reverse(result_so_far)

Then you check the length of last_sublist. If it corresponds to chunk_length, then you are fine, result_so_far has the desired result. In the case it is smaller than chunk_length, you will need to incorporate its elements with the first sublist of result_so_far, which you can do as follows: [first_sublist | rest ] = Enum.reverse(other_sublists)

[Enum.concat(first_sublist, last_sublist) | rest] should then render

[[1, 2, 7], [3, 4], [5, 6]]

Upvotes: 8

Related Questions