Get And Forget

Sat, Dec 17, 2016

File processing is always an important part of any programming language. In Elixir its quite easy with the IO and the File modules. The problem at hand was to read in a csv file and then utilize logic to cast all of the keys and values for further calculations (the total with the sales tax). The csv kinda looked like:

id,ship_to,net_amount
123,:NC,50.00

The hints given for the methods include to use IO.stream along with a File.read. Beginnings looked like:

defmodule Sales do

  def calculate_net_amounts_from_file(file_path) do
    {:ok, file } = File.open(file_path, [:read])
    entire_read_file_data = Enum.map IO.stream(file, :line), &make_order/1
    File.close(file)
  end
  
  def make_order(string) do
  end
end

Pretty straight forward right? Cool. So now I needed to make sure to transform and cast all the input data to make sure all of the calculations could be made correctly. The next portion involved Elixir’s pipeline operator to mold the read data into exactly what I needed looking kind of like:

    casted_keyword_list = keyword_list
                            |> cast_id_value
                            |> cast_ship_to_value
                            |> cast_net_amount_value

This effort allowed me to turn the String data into atom/integer/float types that I need to do the job. Also utilizing Keyword lists (chosen because of its availability of update methods) helped me to pipe my data into calculating the final total amount. Yea buddy.

defmodule Sales do

  def calculate_net_amounts_from_file(file_path) do
    {:ok, file } = File.open(file_path, [:read])
    entire_read_file_data = Enum.map IO.stream(file, :line), &make_order/1
    filtered_read_data = Enum.filter(entire_read_file_data, fn(current_value) -> Enum.count(current_value) > 0 end)
    IO.inspect (filtered_read_data)
    File.close(file)
  end
  
  def make_order("id,ship_to,net_amount\n"), do: []

  def make_order(string) do
    list = String.split(string, ~r{,})
    [id, ship_to, net_amount] = list
    keyword_list = [id: id, ship_to: ship_to, net_amount: net_amount]
    casted_keyword_list = keyword_list
                            |> cast_id_value
                            |> cast_ship_to_value
                            |> cast_net_amount_value

    calculated_net_amounts = add_totals(casted_keyword_list)
  end

  def cast_id_value(keyword_list) do
    {_original, transformed_order_line} = Keyword.get_and_update(keyword_list, :id, fn current_value ->
      { current_value, String.to_integer(current_value) }
    end)
    transformed_order_line
  end

  def cast_ship_to_value(keyword_list) do
    {_original, transformed_order_line} = Keyword.get_and_update(keyword_list, :ship_to, fn current_value ->
      { current_value,
        String.trim_leading(current_value, ":")
          |> String.to_atom
      }
    end)
  transformed_order_line
  end

  def cast_net_amount_value(keyword_list) do
    {_original, transformed_order_line} = Keyword.get_and_update(keyword_list, :net_amount, fn current_value ->
      { current_value,
        String.trim_trailing(current_value)
          |> String.to_float
      }
    end)
  transformed_order_line
  end

  def tax_rates do
    [ NC: 0.075, TX: 0.08 ]
  end

  def add_totals(order) do
    total_amount = order[:net_amount] + (order[:net_amount] * (tax_rates[order[:ship_to]] || 0 ))
    Keyword.put_new(order, :total_amount, total_amount)
  end
end

Most importantly what I want you to notice here is the usage of the get_and_update function. At first when I started using it I thought it was kind of weird. Reasons including that in order for it to work you must return a Tuple and not just any Tuple, but a Tuple that includes the first value being the original value and the second part the entire data structure including the brand new do-hickey. To me it seemed like a bit much to get what I needed (thats what I get from coming from Ruby land). However, I realized that the beauty of this is that since we are having to include that original dude we are having a helper in the fight against bugs. Debugging will be an x-amount easier because we will know at this point exactly where we were. Super cool. Dope. Also you just pattern match the data you need out and you are solid.

defmodule Sales do

  def calculate_net_amounts_from_file(file_path) do
    {:ok, file } = File.open(file_path, [:read])
    entire_read_file_data = Enum.map IO.stream(file, :line), &make_order/1
    filtered_read_data = Enum.filter(entire_read_file_data, fn(current_value) -> Enum.count(current_value) > 0 end)
    IO.inspect (filtered_read_data)
    File.close(file)
  end
  
  def make_order("id,ship_to,net_amount\n"), do: []

  def make_order(string) do
    list = String.split(string, ~r{,})
    [id, ship_to, net_amount] = list
    keyword_list = [id: id, ship_to: ship_to, net_amount: net_amount]
    casted_keyword_list = keyword_list
                            |> cast_id_value
                            |> cast_ship_to_value
                            |> cast_net_amount_value

    calculated_net_amounts = add_totals(casted_keyword_list)
  end

  def cast_id_value(keyword_list) do
    {_original, transformed_order_line} = Keyword.get_and_update(keyword_list, :id, fn current_value ->
      { current_value, String.to_integer(current_value) }
    end)
    transformed_order_line
  end

  def cast_ship_to_value(keyword_list) do
    {_original, transformed_order_line} = Keyword.get_and_update(keyword_list, :ship_to, fn current_value ->
      { current_value,
        String.trim_leading(current_value, ":")
          |> String.to_atom
      }
    end)
  transformed_order_line
  end

  def cast_net_amount_value(keyword_list) do
    {_original, transformed_order_line} = Keyword.get_and_update(keyword_list, :net_amount, fn current_value ->
      { current_value,
        String.trim_trailing(current_value)
          |> String.to_float
      }
    end)
  transformed_order_line
  end

  def tax_rates do
    [ NC: 0.075, TX: 0.08 ]
  end

  def add_totals(order) do
    total_amount = order[:net_amount] + (order[:net_amount] * (tax_rates[order[:ship_to]] || 0 ))
    Keyword.put_new(order, :total_amount, total_amount)
  end
end

Now we have calculated totals and we can charge and make that cash.