1
votes

given an Enum of integers (that comes from an external source), I need to convert them to string, and join them (separated by space).

The 2nd part is to split the resulting string in multiple chunks, with each chunk not exceeding 100 characters and the split cannot be in the middle of a number (so the split can only happen on the spaces that were inserted between the numbers).

The first part is trivial. Actually keeping the size of the resulting strings under the limit is the challenge.

Looked at Enum.chunk_while/4 but not sure this is the right approach. I also explored splitting the list while it's integers and trying to join the results but no luck.

4

4 Answers

2
votes

Here's a function, join_len/2, that does it in one pass. It works by building up each string until it reaches 100 characters, and then moves on to the next string.

defmodule Example do
  def join_len(enum, len), do: join_len(enum, "", [], len)

  defp join_len([], curr, done, _len), do: Enum.reverse([curr | done])

  defp join_len([num | rest], curr, done, len) do
    str = Integer.to_string(num)
    size = byte_size(str)
    curr_size = byte_size(curr)

    cond do
      curr_size == 0 -> join_len(rest, str, done, len)
      curr_size + size + 1 <= len -> join_len(rest, "#{curr} #{str}", done, len)
      curr_size + size + 1 > len -> join_len(rest, str, [curr | done], len)
    end
  end

  def test do
    nums = for _i <- 1..20, do: 1_234_567_890
    join_len(nums, 100)
  end
end

Running the test produces:

[
    "1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890",
    "1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890",
    "1234567890 1234567890"
]
2
votes

If the total length of the resulting string is not extremely huge, and you are fine with joining all numbers first (as you stated in the original question,) the solution involving Regex.scan/2 might be move succinct.

nums = for _i <- 1..20, do: 1_234_567_890
Regex.scan(~r/\d.{1,99}(?=\s|\Z)/, Enum.join(nums, " "))

#⇒ [
#    ["1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890"],
#    ["1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890"],
#    ["1234567890 1234567890"]
#  ]

The regular expression here looks for a digit followed by at most 99 symbols greedily, followed by a space or end of input (the latter is not included in the match.)

1
votes

this is for completeness. the answer Adam posted and I accepted is more succinct and to the point

def max_size_str(enumin, spacing) do
chunk_fun = fn element, acc ->
  acc_sum = acc |> Enum.map(fn x -> List.last(x) end) |> Enum.sum()

  if acc_sum + List.last(element) <= spacing do
    if acc == [[0, 0]] do
      {:cont, [element]}
    else
      {:cont, [element | acc]}
    end
  else
    {:cont, acc, [element]}
  end
end

after_fun = fn
  element ->
    {:cont, element, []}
end

enumin
|> Enum.map(fn x -> [x, String.length(Integer.to_string(x)) + 1] end)
|> Enum.chunk_while([[0, 0]], chunk_fun, after_fun)
|> Enum.map(&Enum.reverse(&1))
|> Enum.map(fn x -> x |> Enum.map(&List.first(&1)) end)
|> Enum.map(&Enum.join(&1, " "))

end
1
votes

Just out of curiosity, here is a slightly modified Adam’s solution that uses pattern matching in function clauses to achieve the same result.

defmodule Example do
  def join_len(enum, len, acc \\ [])

  def join_len([], _len, acc), do: Enum.reverse(acc)

  def join_len([num | rest], len, acc),
    do: join_len(rest, len, join_num("#{num}", len, acc))
    
  defp join_num(num, _, []), do: [num]
  defp join_num(num, len, [head|tail])
      when byte_size(num) + byte_size(head) < len,
    do: [head <> " " <> num | tail]
  defp join_num(num, _, acc), do: [num | acc]

  def test do
    nums = for _i <- 1..20, do: 1_234_567_890
    join_len(nums, 100)
  end
end