|
| 1 | +defmodule Ledger do |
| 2 | + @doc """ |
| 3 | + Format the given entries given a currency and locale |
| 4 | + """ |
| 5 | + @type currency :: :usd | :eur |
| 6 | + @type locale :: :en_US | :nl_NL |
| 7 | + @type entry :: %{amount_in_cents: integer(), date: Date.t(), description: String.t()} |
| 8 | + |
| 9 | + @spec format_entries(currency(), locale(), list(entry())) :: String.t() |
| 10 | + def format_entries(currency, locale, entries) do |
| 11 | + header = header(locale) |
| 12 | + |
| 13 | + entries = |
| 14 | + entries |
| 15 | + |> Enum.sort(&compare_entries/2) |
| 16 | + |> Enum.map(fn entry -> format_entry(currency, locale, entry) end) |
| 17 | + |
| 18 | + Enum.join([header | entries], "\n") <> "\n" |
| 19 | + end |
| 20 | + |
| 21 | + defp header(:en_US), do: "Date | Description | Change " |
| 22 | + defp header(:nl_NL), do: "Datum | Omschrijving | Verandering " |
| 23 | + |
| 24 | + defp compare_entries(a, b) do |
| 25 | + case Date.compare(a.date, b.date) do |
| 26 | + :lt -> |
| 27 | + true |
| 28 | + |
| 29 | + :gt -> |
| 30 | + false |
| 31 | + |
| 32 | + :eq -> |
| 33 | + cond do |
| 34 | + a.description < b.description -> true |
| 35 | + a.description > b.description -> false |
| 36 | + true -> a.amount_in_cents <= b.amount_in_cents |
| 37 | + end |
| 38 | + end |
| 39 | + end |
| 40 | + |
| 41 | + @description_width 25 |
| 42 | + @amount_width 13 |
| 43 | + defp format_entry(currency, locale, %{ |
| 44 | + amount_in_cents: amount, |
| 45 | + date: date, |
| 46 | + description: description |
| 47 | + }) do |
| 48 | + date = format_date(date, locale) |
| 49 | + |
| 50 | + amount = |
| 51 | + amount |
| 52 | + |> format_amount(currency, locale) |
| 53 | + |> String.pad_leading(@amount_width, " ") |
| 54 | + |
| 55 | + description = |
| 56 | + if String.length(description) > @description_width do |
| 57 | + String.slice(description, 0, @description_width - 3) <> "..." |
| 58 | + else |
| 59 | + String.pad_trailing(description, @description_width, " ") |
| 60 | + end |
| 61 | + |
| 62 | + Enum.join([date, description, amount], " | ") |
| 63 | + end |
| 64 | + |
| 65 | + defp format_date(date, :en_US) do |
| 66 | + year = date.year |
| 67 | + month = date.month |> to_string() |> String.pad_leading(2, "0") |
| 68 | + day = date.day |> to_string() |> String.pad_leading(2, "0") |
| 69 | + Enum.join([month, day, year], "/") |
| 70 | + end |
| 71 | + |
| 72 | + defp format_date(date, :nl_NL) do |
| 73 | + year = date.year |
| 74 | + month = date.month |> to_string() |> String.pad_leading(2, "0") |
| 75 | + day = date.day |> to_string() |> String.pad_leading(2, "0") |
| 76 | + Enum.join([day, month, year], "-") |
| 77 | + end |
| 78 | + |
| 79 | + defp format_amount(amount, currency, :en_US) do |
| 80 | + currency = format_currency(currency) |
| 81 | + number = format_number(abs(amount), ".", ",") |
| 82 | + |
| 83 | + if amount >= 0 do |
| 84 | + " #{currency}#{number} " |
| 85 | + else |
| 86 | + "(#{currency}#{number})" |
| 87 | + end |
| 88 | + end |
| 89 | + |
| 90 | + defp format_amount(amount, currency, :nl_NL) do |
| 91 | + currency = format_currency(currency) |
| 92 | + number = format_number(abs(amount), ",", ".") |
| 93 | + |
| 94 | + if amount >= 0 do |
| 95 | + "#{currency} #{number} " |
| 96 | + else |
| 97 | + "#{currency} -#{number} " |
| 98 | + end |
| 99 | + end |
| 100 | + |
| 101 | + defp format_currency(:usd), do: "$" |
| 102 | + defp format_currency(:eur), do: "€" |
| 103 | + |
| 104 | + defp format_number(number, decimal_separator, thousand_separator) do |
| 105 | + decimal = number |> rem(100) |> to_string() |> String.pad_leading(2, "0") |
| 106 | + whole = number |> div(100) |> to_string() |> chunk() |> Enum.join(thousand_separator) |
| 107 | + whole <> decimal_separator <> decimal |
| 108 | + end |
| 109 | + |
| 110 | + defp chunk(number) do |
| 111 | + case String.length(number) do |
| 112 | + 0 -> |
| 113 | + [] |
| 114 | + |
| 115 | + n when n < 3 -> |
| 116 | + [number] |
| 117 | + |
| 118 | + n when rem(n, 3) == 0 -> |
| 119 | + {chunk, rest} = String.split_at(number, 3) |
| 120 | + [chunk | chunk(rest)] |
| 121 | + |
| 122 | + n -> |
| 123 | + {chunk, rest} = String.split_at(number, rem(n, 3)) |
| 124 | + [chunk | chunk(rest)] |
| 125 | + end |
| 126 | + end |
| 127 | +end |
0 commit comments