16 Jul 2013, 02:25
Dave_gnome_head_isolated_pragsmall

Dave Thomas (337 posts)

  • The Lists chapter had an exercise about calculating sales tax. We now have the sales information in a file of comma-separated id, ship_to, and amount values. The file looks like this:

    id,ship_to,net_amount
    123,:NC,100.00
    124,:OK,35.50
    125,:TX,24.00
    126,:TX,44.80
    127,:NC,25.00
    128,:MA,10.00
    129,:CA,102.00
    120,:NC,50.00
    

    Write a function that reads and parses this file, and then passes the result to the sales tax function. Remember that the data should be formatted into a keyword list, and that the fields need to be the correct types (so the id field is an integer, and so on).

    You’ll need the library functions File.open, IO.read(file, :line), and IO.stream(file).

A Possible Solution
defmodule SimpleCSV do
  def read(filename) do
    file = File.open!(filename)
    headers = read_headers(IO.read(file, :line))
    Enum.map(IO.stream(file), create_one_row(headers, &1))
  end

  defp read_headers(hdr_line) do
    from_csv_and_map(hdr_line, binary_to_atom(&1))
  end

  defp create_one_row(headers, row_csv) do
    row = from_csv_and_map(row_csv, maybe_convert_numbers(&1))
    Enum.zip(headers, row)
  end

  defp from_csv_and_map(row_csv, mapper) do
    row_csv
    |> String.strip
    |> String.split(%r{,\s*})
    |> Enum.map(mapper)
  end

  defp maybe_convert_numbers(value) do
    cond do
      Regex.match?(%r{^\d+$}, value)           -> binary_to_integer(value)
      Regex.match?(%r{^\d+\.\d+$}, value)      -> binary_to_float(value)
      << ?: :: utf8, name :: binary >> = value -> binary_to_atom(name)
      true -> value                                            
    end
  end
end

defmodule Tax do

  def orders_with_total(orders, tax_rates) do
    orders |> Enum.map(add_total_to(&1, tax_rates))
  end

  def add_total_to(order = [id: _, ship_to: state, net_amount: net], tax_rates) do
    tax_rate = Keyword.get(tax_rates, state, 0)
    tax      = net*tax_rate
    total    = net+tax
    Keyword.put(order, :total_amount, total)
  end

end


tax_rates =  [ NC: 0.075, TX: 0.08 ]

orders = SimpleCSV.read("sales_data.csv")

IO.inspect Tax.orders_with_total(orders, tax_rates)


29 Apr 2014, 18:46
Ernie2_pragsmall

Ernie Miller (4 posts)

For Elixir 0.13, this solution seems to need updates to use captures. For instance:

# previous:
# create_one_row(headers, &1)
&create_one_row(headers, &1)

Also, IO.stream now requires :line as a second parameter.

Enum.map(IO.stream(file, :line), &create_one_row(headers, &1))

Oh, also the sigil for regexps should be changed from % to ~.

  You must be logged in to comment