Convert this python code into elixir

How would you write the accepted answer of https://stackoverflow.com/questions/19293481/how-to-elegantly-interleave-two-lists-of-uneven-length-in-python in Elixir?

More specifically, this code

from itertools import groupby

len_ab = len(a) + len(b)
groups = groupby(((a[len(a)*i//len_ab], b[len(b)*i//len_ab]) for i in range(len_ab)),
                 key=lambda x:x[0])
[j[i] for k,g in groups for i,j in enumerate(g)]

How about this one:

defmodule Example do
  @spec sample(list(), list(), non_neg_integer()) :: list()
  # ensures that parameters are in correct types
  # calculates evenly spacing
  def sample(first, second, rotation \\ 0)
      when is_list(first) and is_list(second) and is_integer(rotation) and rotation >= 0 do
    first_length = length(first)
    second_length = length(second)

    if first_length > second_length do
      every = div(first_length, second_length)
      do_sample(first, second, every, rotation)
    else
      every = div(second_length, first_length)
      do_sample(second, first, every, rotation)
    end
  end

  # make sure that rotation does not conflicts with evenly spacing
  defp do_sample(first, second, every, rotation) when rotation > every do
    corrected_rotation = rem(rotation, every)
    do_sample(first, second, every, corrected_rotation)
  end

  # for each indexed element
  # in reversed order (important for inserting index conflicts)
  # call custom insert with index calculated by index of second list, every and rotation
  defp do_sample(first, second, every, rotation) do
    second
    |> Enum.with_index()
    |> Enum.reverse()
    |> Enum.reduce(first, &custom_insert(&1, &2, every, rotation))
  end

  # small pattern-matching optimization
  defp custom_insert({element, 0}, acc, _every, rotation),
    do: List.insert_at(acc, rotation, element)

  # small pattern-matching optimization
  defp custom_insert({element, 1}, acc, every, rotation),
    do: List.insert_at(acc, every + rotation, element)

  defp custom_insert({element, index}, acc, every, rotation),
    do: List.insert_at(acc, every * index + rotation, element)
end

a = Enum.to_list(1..4)     # range to list: [1, 2, 3, 4]
b = String.graphemes("ab") # string to grapheme list: ["a", "b"]
Example.sample(a, b)       # ["a", 1, 2, "b", 3, 4]
Example.sample(a, b, 0)    # ["a", 1, 2, "b", 3, 4]
Example.sample(a, b, 1)    # [1, "a", 2, 3, "b", 4]
Example.sample(a, b, 2)    # [1, 2, "a", 3, 4, "b"]
Example.sample(a, b, 3)    # [1, "a", 2, 3, "b", 4]
Example.sample(a, b, 4)    # ["a", 1, 2, "b", 3, 4]
Example.sample(a, b, 5)    # [1, "a", 2, 3, "b", 4]

Wow! This should fit the bill. Thank you!!!

here’s a slightly more idiomatic version, although I think there might be some corner cases where it’s not as even as it could be:

defmodule M do 

  @doc """
  interleaves a short list b, into a long list a, such that the
  presence of the short list is evenly spaced in the long list.

  iex> M.interleave([1,2,3,4,5,6,7],[:a, :b])
  [1, 2, :a, 3, 4, 5, :b, 6, 7]
  """
  def interleave(a, b) do
    a_ct = Enum.count(a)
    b_ct = Enum.count(b)

    # calculates how many items are in the cycle of drawing from a vs b.
    recurrence = div(a_ct, b_ct) + 1

    # calculate how much we have to offset the recurrence by to center it.
    offset = div(a_ct + b_ct - ((b_ct - 1) * recurrence), 2)

    # bootstrap the initial condition
    interleave(a, b, offset, recurrence, [])
  end

  # terminating condition: we've exhausted all the items; since we put values on the front
  # in the tail calls, reverse the list.
  def interleave([], [], _, _, rev_list), do: Enum.reverse(rev_list)
  def interleave(a, [b_hd | b_tl], n, recurrence, list_so_far) 
    when rem(n, recurrence) == 0 do
    # draw from the shorter list every <recurrence>.  Put on front
    # of the list we're building, then tail call in.
    interleave(a, b_tl, n + 1, recurrence, [b_hd | list_so_far])
  end
  def interleave([a_hd | a_tl], b, n, recurrence, list_so_far) do
    # draw from the longer list.  Put on front of the list we're building, 
    # then tail call in.
    interleave(a_tl, b, n + 1, recurrence, [a_hd | list_so_far])
  end  

end

if the enum.reverse thing is a bit confusing, you can also do this:

def interleave([], [], _, _, list), do: list
def interleave(a, [b_hd | b_tl], n, interval, list_so_far) 
  when rem(n, interval) == 0 do
  interleave(a, b_tl, n + 1, interval, list_so_far ++ [b_hd])
end
def interleave([a_hd | a_tl], b, n, interval, list_so_far) do
  interleave(a_tl, b, n + 1, interval, list_so_far ++ [a_hd])
end  
1 Like

Nice work …