Added Swaziland as invalid destination

Added Swaziland as invalid destination

defmodule Shippex.Carrier.USPS do
  @moduledoc false
  @behaviour Shippex.Carrier

  require EEx
  import SweetXml
  import Shippex.Address, only: [state_without_country: 1]

  alias Shippex.Carrier.USPS.Client
  alias Shippex.{Address, Package, Label, Service, Shipment, Util, ISO}

  @default_container :rectangular
  @large_containers ~w(rectangular nonrectangular variable)a

  for f <- ~w(address cancel label rate validate_address)a do
    EEx.function_from_file(:defp, :"render_#{f}", __DIR__ <> "/usps/templates/#{f}.eex", [
      :assigns
    ])
  end

  defmacro with_response(response, do: block) do
    quote do
      case unquote(response) do
        {:ok, %{body: body}} ->
          case xpath(body, ~x"//Error//text()"s) do
            "" ->
              var!(body) = body
              unquote(block)

            error ->
              code = xpath(body, ~x"//Error//Number//text()"s)
              message = xpath(body, ~x"//Error//Description//text()"s)
              {:error, %{code: code, message: message}}
          end

        {:error, _} ->
          {:error, %{code: 1, message: "The USPS API is down."}}
      end
    end
  end

  def fetch_rates(_shipment) do
    raise "Not implemented for USPS"
  end

  def fetch_rate(%Shipment{} = shipment, service) do
    service =
      case service do
        %Shippex.Service{} = service -> service
        s when is_atom(s) -> Service.get(s)
      end

    api =
      if shipment.international? do
        "IntlRateV2"
      else
        "RateV4"
      end

    rate = render_rate(shipment: shipment, service: service)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: rate}) do
      spec =
        extra_services_spec(shipment) ++
          if shipment.international? do
            [
              name: ~x"./SvcDescription//text()"s,
              service: ~x"./SvcDescription//text()"s,
              rate: ~x"./Postage//text()"f
            ]
          else
            [
              name: ~x"./MailService//text()"s,
              service: ~x"./MailService//text()"s,
              rate: ~x"./Rate//text()"s
            ]
          end

      rates =
        if shipment.international? do
          xpath(
            body,
            ~x"//IntlRateV2Response//Package//Service"l,
            spec
          )
        else
          xpath(
            body,
            ~x"//RateV4Response//Package//Postage"l,
            spec
          )
        end
        |> add_line_items(shipment, service)
        |> Enum.map(fn response ->
          total = response.line_items |> Enum.map(& &1.price) |> Enum.sum()

          %{
            name: strip_html(response.name),
            service: description_to_service(response.service),
            rate: total,
            line_items: response.line_items
          }
        end)
        |> Enum.map(fn %{name: description, service: service} = response ->
          service = %{service | description: description}

          rate = %Shippex.Rate{
            service: service,
            price: response.rate,
            line_items: response.line_items
          }

          {:ok, rate}
        end)

      rates =
        if shipment.international? do
          rates
          |> Enum.sort(fn {:ok, rate1}, {:ok, rate2} ->
            service = String.downcase(service.description)

            d1 = String.jaro_distance(String.downcase(rate1.service.description), service)
            d2 = String.jaro_distance(String.downcase(rate2.service.description), service)

            d1 > d2
          end)
        else
          rates
        end

      case rates do
        [] -> {:error, "Rate unavailable for service."}
        [rate] -> rate
        list when is_list(list) -> hd(list)
      end
    end
  end

  def create_transaction(shipment, service) when is_atom(service) do
    create_transaction(shipment, Service.get(service))
  end

  def create_transaction(%Shipment{} = shipment, %Service{} = service) do
    api =
      cond do
        not shipment.international? ->
          "eVS"

        service.id == :usps_priority_express ->
          "eVSExpressMailIntl"

        service.id == :usps_priority ->
          "eVSPriorityMailIntl"

        true ->
          raise """
          Only the Priority and Priority Express services are supported for
          international shipments at the moment.
          """
      end

    request = render_label(shipment: shipment, service: service, api: api)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: request}) do
      spec =
        if shipment.international? do
          [insurance_fee: ~x"//InsuranceFee//text()"s]
        else
          extra_services_spec(shipment, "Extra")
        end ++
          [
            rate: ~x"//Postage//text()"s,
            tracking_number: ~x"//BarcodeNumber//text()"s,
            image: ~x"//LabelImage//text()"s
          ]

      response = xpath(body, ~x"//#{api}Response", spec) |> add_line_items(shipment, service)

      line_items = response.line_items
      price = line_items |> Enum.map(& &1.price) |> Enum.sum()

      rate = %Shippex.Rate{service: service, price: price, line_items: line_items}
      image = String.replace(response.image, "\n", "")
      label = %Label{tracking_number: response.tracking_number, format: :pdf, image: image}

      transaction = Shippex.Transaction.new(shipment, rate, label)

      {:ok, transaction}
    end
  end

  def cancel_transaction(%Shippex.Transaction{} = transaction) do
    cancel_transaction(transaction.shipment, transaction.label.tracking_number)
  end

  def cancel_transaction(%Shippex.Shipment{} = shipment, tracking_number) do
    root =
      if shipment.international? do
        "eVSI"
      else
        "eVS"
      end

    api = root <> "Cancel"

    request = render_cancel(root: root, tracking_number: tracking_number)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: request}) do
      data =
        xpath(
          body,
          ~x"//#{root}CancelResponse",
          status: ~x"//Status//text()"s,
          reason: ~x"//Reason//text()"s
        )

      status =
        if data.status =~ ~r/not cancel/i do
          :error
        else
          :ok
        end

      {status, data.reason}
    end
  end

  def us_territories() do
    ~w(PR PW MH FM MP GU AS VI)
  end

  def invalid_destinations() do
    ~w(AN AQ BV EH KP HM IO PN SO SJ SY SZ TF YE YU)
  end

  defp extra_services_spec(shipment, prefix \\ nil) do
    prefix =
      case prefix do
        nil ->
          if shipment.international?, do: "Extra", else: "Special"

        prefix ->
          prefix
      end

    [
      extra_services: [
        ~x"//#{prefix}Services//#{prefix}Service"l,
        id: ~x"./ServiceID//text()"s,
        name: ~x"./ServiceName//text()"s,
        available: ~x"./Available//text()"s |> transform_by(&String.downcase/1),
        price: ~x"./Price//text()"s
      ]
    ]
  end

  defp add_line_items(rates, shipment, service) when is_list(rates) do
    Enum.map(rates, fn rate -> add_line_items(rate, shipment, service) end)
  end

  defp add_line_items(rate, shipment, service) do
    postage_line_item = %{name: "Postage", price: rate.rate}

    insurance_line_item =
      cond do
        is_nil(shipment.package.insurance) ->
          nil

        not is_nil(rate[:insurance_fee]) ->
          %{name: "Insurance", price: rate.insurance_fee}

        true ->
          insurance_code = insurance_code(shipment, service)

          rate.extra_services
          |> Enum.find(fn
            %{available: available, id: ^insurance_code} when available != "false" -> true
            _ -> false
          end)
          |> case do
            %{price: price} ->
              %{name: "Insurance", price: price}

            _ ->
              nil
          end
      end

    line_items =
      [postage_line_item, insurance_line_item]
      |> Enum.reject(&is_nil/1)
      |> Enum.map(fn %{price: price} = line_item ->
        %{line_item | price: Util.price_to_cents(price)}
      end)

    Map.put(rate, :line_items, line_items)
  end

  defp insurance_code(%{international?: true}, %{id: :usps_gxg}), do: "106"
  defp insurance_code(%{international?: true}, %{id: :usps_priority}), do: "108"
  defp insurance_code(%{international?: true}, %{id: :usps_priority_express}), do: "107"
  defp insurance_code(%{international?: false}, %{id: :usps_priority}), do: "125"
  defp insurance_code(%{international?: false}, %{id: :usps_priority_express}), do: "101"
  defp insurance_code(%{international?: false}, %{id: _}), do: "100"

  defp weight_in_ounces(%Shipment{package: %Package{weight: weight}}) do
    16 *
      case Application.get_env(:shippex, :weight_unit, :lbs) do
        :lbs ->
          weight

        :kg ->
          Util.kgs_to_lbs(weight)

        u ->
          raise """
          Invalid unit of measurement specified: #{IO.inspect(u)}

          Must be either :lbs or :kg
          """
      end
  end

  defp description_to_service(description) do
    cond do
      description =~ ~r/priority mail express/i ->
        :usps_priority_express

      description =~ ~r/priority/i ->
        :usps_priority

      description =~ ~r/first[-\s]*class/i ->
        :usps_first_class

      description =~ ~r/retail ground/i ->
        :usps_retail_ground

      description =~ ~r/media mail/i ->
        :usps_media

      description =~ ~r/library mail/i ->
        :usps_library

      description =~ ~r/gxg/i ->
        :usps_gxg
    end
    |> Shippex.Service.get()
  end

  defp international_mail_type(%Package{container: nil}), do: "PACKAGE"

  defp international_mail_type(%Package{container: container}) do
    container = "#{container}"

    cond do
      container =~ ~r/envelope/i -> "ENVELOPE"
      container =~ ~r/flat[-\s]*rate/i -> "FLATRATE"
      container =~ ~r/rectangular|variable/i -> "PACKAGE"
      true -> "ALL"
    end
  end

  def validate_address(%Shippex.Address{country: "US"} = address) do
    request = render_validate_address(address: address)

    with_response Client.post("", %{API: "Verify", XML: request}) do
      candidates =
        body
        |> xpath(
          ~x"//AddressValidateResponse//Address"l,
          # USPS swaps address lines 1 & 2
          address: ~x"./Address2//text()"s,
          address_line_2: ~x"./Address1//text()"s,
          city: ~x"./City//text()"s,
          state: ~x"./State//text()"s,
          zip: ~x"./Zip5//text()"s
        )
        |> Enum.map(fn candidate ->
          candidate
          |> Map.merge(Map.take(address, ~w(first_name last_name name company_name phone)a))
          |> Shippex.Address.new!()
        end)

      {:ok, candidates}
    end
  end

  defp country(%Address{country: code}) do
    ISO.abbreviation_to_country_name(code)
    |> replace_name()
  end

  defp replace_name("Russian Federation"), do: "Russia"
  defp replace_name("Korea (South)"), do: "South Korea"
  defp replace_name("Korea (North)"), do: "North Korea"
  defp replace_name("Congo, the"), do: "Congo, Republic of the"
  defp replace_name("Falkland Islands (Malvinas)"), do: "Falkland Islands"
  defp replace_name("Holy See"), do: "Vatican City"
  defp replace_name("Cocos (Keeling) Islands"), do: "Cocos Island (Australia)"
  defp replace_name("Cote d'Ivoire"), do: "Ivory Coast"
  defp replace_name("Bosnia & Herzegovina"), do: "Bosnia-Herzegovina"

  defp replace_name("South Georgia and the South Sandwich Islands"),
    do: "South Georgia (Falkland Islands)"

  defp replace_name(name), do: String.replace(name, "&", "and")

  defp container(%Shipment{package: package}) do
    case Package.usps_containers()[package.container] do
      nil -> Package.usps_containers()[@default_container]
      container -> container
    end
    |> String.upcase()
  end

  defp size(%Shipment{package: package} = shipment) do
    is_large? =
      cond do
        container(shipment) == "RECTANGULAR" ->
          true

        package.container in @large_containers ->
          package
          |> Map.take(~w(large width height)a)
          |> Map.values()
          |> Enum.any?(&(&1 > 12))

        true ->
          false
      end

    if is_large?, do: "LARGE", else: "REGULAR"
  end

  defp strip_html(string) do
    string
    |> HtmlEntities.decode()
    |> String.replace(~r/<\/?\w+>.*<\/\w+>/, "")
  end

  defp config do
    with cfg when is_list(cfg) <- Keyword.get(Shippex.config(), :usps, {:error, :not_found}),
         un <- Keyword.get(cfg, :username, {:error, :not_found, :username}),
         pw <- Keyword.get(cfg, :password, {:error, :not_found, :password}) do
      %{username: un, password: pw}
    else
      {:error, :not_found, token} ->
        raise Shippex.InvalidConfigError, message: "USPS config key missing: #{token}"

      {:error, :not_found} ->
        raise Shippex.InvalidConfigError, message: "USPS config is either invalid or not found."
    end
  end
end

defmodule Shippex.Carrier.USPS do
  @moduledoc false
  @behaviour Shippex.Carrier

  require EEx
  import SweetXml
  import Shippex.Address, only: [state_without_country: 1]

  alias Shippex.Carrier.USPS.Client
  alias Shippex.{Address, Package, Label, Service, Shipment, Util, ISO}

  @default_container :rectangular
  @large_containers ~w(rectangular nonrectangular variable)a

  for f <- ~w(address cancel label rate validate_address)a do
    EEx.function_from_file(:defp, :"render_#{f}", __DIR__ <> "/usps/templates/#{f}.eex", [
      :assigns
    ])
  end

  defmacro with_response(response, do: block) do
    quote do
      case unquote(response) do
        {:ok, %{body: body}} ->
          case xpath(body, ~x"//Error//text()"s) do
            "" ->
              var!(body) = body
              unquote(block)

            error ->
              code = xpath(body, ~x"//Error//Number//text()"s)
              message = xpath(body, ~x"//Error//Description//text()"s)
              {:error, %{code: code, message: message}}
          end

        {:error, _} ->
          {:error, %{code: 1, message: "The USPS API is down."}}
      end
    end
  end

  def fetch_rates(_shipment) do
    raise "Not implemented for USPS"
  end

  def fetch_rate(%Shipment{} = shipment, service) do
    service =
      case service do
        %Shippex.Service{} = service -> service
        s when is_atom(s) -> Service.get(s)
      end

    api =
      if shipment.international? do
        "IntlRateV2"
      else
        "RateV4"
      end

    rate = render_rate(shipment: shipment, service: service)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: rate}) do
      spec =
        extra_services_spec(shipment) ++
          if shipment.international? do
            [
              name: ~x"./SvcDescription//text()"s,
              service: ~x"./SvcDescription//text()"s,
              rate: ~x"./Postage//text()"f
            ]
          else
            [
              name: ~x"./MailService//text()"s,
              service: ~x"./MailService//text()"s,
              rate: ~x"./Rate//text()"s
            ]
          end

      rates =
        if shipment.international? do
          xpath(
            body,
            ~x"//IntlRateV2Response//Package//Service"l,
            spec
          )
        else
          xpath(
            body,
            ~x"//RateV4Response//Package//Postage"l,
            spec
          )
        end
        |> add_line_items(shipment, service)
        |> Enum.map(fn response ->
          total = response.line_items |> Enum.map(& &1.price) |> Enum.sum()

          %{
            name: strip_html(response.name),
            service: description_to_service(response.service),
            rate: total,
            line_items: response.line_items
          }
        end)
        |> Enum.map(fn %{name: description, service: service} = response ->
          service = %{service | description: description}

          rate = %Shippex.Rate{
            service: service,
            price: response.rate,
            line_items: response.line_items
          }

          {:ok, rate}
        end)

      rates =
        if shipment.international? do
          rates
          |> Enum.sort(fn {:ok, rate1}, {:ok, rate2} ->
            service = String.downcase(service.description)

            d1 = String.jaro_distance(String.downcase(rate1.service.description), service)
            d2 = String.jaro_distance(String.downcase(rate2.service.description), service)

            d1 > d2
          end)
        else
          rates
        end

      case rates do
        [] -> {:error, "Rate unavailable for service."}
        [rate] -> rate
        list when is_list(list) -> hd(list)
      end
    end
  end

  def create_transaction(shipment, service) when is_atom(service) do
    create_transaction(shipment, Service.get(service))
  end

  def create_transaction(%Shipment{} = shipment, %Service{} = service) do
    api =
      cond do
        not shipment.international? ->
          "eVS"

        service.id == :usps_priority_express ->
          "eVSExpressMailIntl"

        service.id == :usps_priority ->
          "eVSPriorityMailIntl"

        true ->
          raise """
          Only the Priority and Priority Express services are supported for
          international shipments at the moment.
          """
      end

    request = render_label(shipment: shipment, service: service, api: api)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: request}) do
      spec =
        if shipment.international? do
          [insurance_fee: ~x"//InsuranceFee//text()"s]
        else
          extra_services_spec(shipment, "Extra")
        end ++
          [
            rate: ~x"//Postage//text()"s,
            tracking_number: ~x"//BarcodeNumber//text()"s,
            image: ~x"//LabelImage//text()"s
          ]

      response = xpath(body, ~x"//#{api}Response", spec) |> add_line_items(shipment, service)

      line_items = response.line_items
      price = line_items |> Enum.map(& &1.price) |> Enum.sum()

      rate = %Shippex.Rate{service: service, price: price, line_items: line_items}
      image = String.replace(response.image, "\n", "")
      label = %Label{tracking_number: response.tracking_number, format: :pdf, image: image}

      transaction = Shippex.Transaction.new(shipment, rate, label)

      {:ok, transaction}
    end
  end

  def cancel_transaction(%Shippex.Transaction{} = transaction) do
    cancel_transaction(transaction.shipment, transaction.label.tracking_number)
  end

  def cancel_transaction(%Shippex.Shipment{} = shipment, tracking_number) do
    root =
      if shipment.international? do
        "eVSI"
      else
        "eVS"
      end

    api = root <> "Cancel"

    request = render_cancel(root: root, tracking_number: tracking_number)

    with_response Client.post("ShippingAPI.dll", %{API: api, XML: request}) do
      data =
        xpath(
          body,
          ~x"//#{root}CancelResponse",
          status: ~x"//Status//text()"s,
          reason: ~x"//Reason//text()"s
        )

      status =
        if data.status =~ ~r/not cancel/i do
          :error
        else
          :ok
        end

      {status, data.reason}
    end
  end

  def us_territories() do
    ~w(PR PW MH FM MP GU AS VI)
  end

  def invalid_destinations() do
    ~w(AN AQ BV EH KP HM IO PN SO SJ SY TF YE YU)
  end

  defp extra_services_spec(shipment, prefix \\ nil) do
    prefix =
      case prefix do
        nil ->
          if shipment.international?, do: "Extra", else: "Special"

        prefix ->
          prefix
      end

    [
      extra_services: [
        ~x"//#{prefix}Services//#{prefix}Service"l,
        id: ~x"./ServiceID//text()"s,
        name: ~x"./ServiceName//text()"s,
        available: ~x"./Available//text()"s |> transform_by(&String.downcase/1),
        price: ~x"./Price//text()"s
      ]
    ]
  end

  defp add_line_items(rates, shipment, service) when is_list(rates) do
    Enum.map(rates, fn rate -> add_line_items(rate, shipment, service) end)
  end

  defp add_line_items(rate, shipment, service) do
    postage_line_item = %{name: "Postage", price: rate.rate}

    insurance_line_item =
      cond do
        is_nil(shipment.package.insurance) ->
          nil

        not is_nil(rate[:insurance_fee]) ->
          %{name: "Insurance", price: rate.insurance_fee}

        true ->
          insurance_code = insurance_code(shipment, service)

          rate.extra_services
          |> Enum.find(fn
            %{available: available, id: ^insurance_code} when available != "false" -> true
            _ -> false
          end)
          |> case do
            %{price: price} ->
              %{name: "Insurance", price: price}

            _ ->
              nil
          end
      end

    line_items =
      [postage_line_item, insurance_line_item]
      |> Enum.reject(&is_nil/1)
      |> Enum.map(fn %{price: price} = line_item ->
        %{line_item | price: Util.price_to_cents(price)}
      end)

    Map.put(rate, :line_items, line_items)
  end

  defp insurance_code(%{international?: true}, %{id: :usps_gxg}), do: "106"
  defp insurance_code(%{international?: true}, %{id: :usps_priority}), do: "108"
  defp insurance_code(%{international?: true}, %{id: :usps_priority_express}), do: "107"
  defp insurance_code(%{international?: false}, %{id: :usps_priority}), do: "125"
  defp insurance_code(%{international?: false}, %{id: :usps_priority_express}), do: "101"
  defp insurance_code(%{international?: false}, %{id: _}), do: "100"

  defp weight_in_ounces(%Shipment{package: %Package{weight: weight}}) do
    16 *
      case Application.get_env(:shippex, :weight_unit, :lbs) do
        :lbs ->
          weight

        :kg ->
          Util.kgs_to_lbs(weight)

        u ->
          raise """
          Invalid unit of measurement specified: #{IO.inspect(u)}

          Must be either :lbs or :kg
          """
      end
  end

  defp description_to_service(description) do
    cond do
      description =~ ~r/priority mail express/i ->
        :usps_priority_express

      description =~ ~r/priority/i ->
        :usps_priority

      description =~ ~r/first[-\s]*class/i ->
        :usps_first_class

      description =~ ~r/retail ground/i ->
        :usps_retail_ground

      description =~ ~r/media mail/i ->
        :usps_media

      description =~ ~r/library mail/i ->
        :usps_library

      description =~ ~r/gxg/i ->
        :usps_gxg
    end
    |> Shippex.Service.get()
  end

  defp international_mail_type(%Package{container: nil}), do: "PACKAGE"

  defp international_mail_type(%Package{container: container}) do
    container = "#{container}"

    cond do
      container =~ ~r/envelope/i -> "ENVELOPE"
      container =~ ~r/flat[-\s]*rate/i -> "FLATRATE"
      container =~ ~r/rectangular|variable/i -> "PACKAGE"
      true -> "ALL"
    end
  end

  def validate_address(%Shippex.Address{country: "US"} = address) do
    request = render_validate_address(address: address)

    with_response Client.post("", %{API: "Verify", XML: request}) do
      candidates =
        body
        |> xpath(
          ~x"//AddressValidateResponse//Address"l,
          # USPS swaps address lines 1 & 2
          address: ~x"./Address2//text()"s,
          address_line_2: ~x"./Address1//text()"s,
          city: ~x"./City//text()"s,
          state: ~x"./State//text()"s,
          zip: ~x"./Zip5//text()"s
        )
        |> Enum.map(fn candidate ->
          candidate
          |> Map.merge(Map.take(address, ~w(first_name last_name name company_name phone)a))
          |> Shippex.Address.new!()
        end)

      {:ok, candidates}
    end
  end

  defp country(%Address{country: code}) do
    ISO.abbreviation_to_country_name(code)
    |> replace_name()
  end

  defp replace_name("Russian Federation"), do: "Russia"
  defp replace_name("Korea (South)"), do: "South Korea"
  defp replace_name("Korea (North)"), do: "North Korea"
  defp replace_name("Congo, the"), do: "Congo, Republic of the"
  defp replace_name("Falkland Islands (Malvinas)"), do: "Falkland Islands"
  defp replace_name("Holy See"), do: "Vatican City"
  defp replace_name("Cocos (Keeling) Islands"), do: "Cocos Island (Australia)"
  defp replace_name("Cote d'Ivoire"), do: "Ivory Coast"
  defp replace_name("Bosnia & Herzegovina"), do: "Bosnia-Herzegovina"

  defp replace_name("South Georgia and the South Sandwich Islands"),
    do: "South Georgia (Falkland Islands)"

  defp replace_name(name), do: String.replace(name, "&", "and")

  defp container(%Shipment{package: package}) do
    case Package.usps_containers()[package.container] do
      nil -> Package.usps_containers()[@default_container]
      container -> container
    end
    |> String.upcase()
  end

  defp size(%Shipment{package: package} = shipment) do
    is_large? =
      cond do
        container(shipment) == "RECTANGULAR" ->
          true

        package.container in @large_containers ->
          package
          |> Map.take(~w(large width height)a)
          |> Map.values()
          |> Enum.any?(&(&1 > 12))

        true ->
          false
      end

    if is_large?, do: "LARGE", else: "REGULAR"
  end

  defp strip_html(string) do
    string
    |> HtmlEntities.decode()
    |> String.replace(~r/<\/?\w+>.*<\/\w+>/, "")
  end

  defp config do
    with cfg when is_list(cfg) <- Keyword.get(Shippex.config(), :usps, {:error, :not_found}),
         un <- Keyword.get(cfg, :username, {:error, :not_found, :username}),
         pw <- Keyword.get(cfg, :password, {:error, :not_found, :password}) do
      %{username: un, password: pw}
    else
      {:error, :not_found, token} ->
        raise Shippex.InvalidConfigError, message: "USPS config key missing: #{token}"

      {:error, :not_found} ->
        raise Shippex.InvalidConfigError, message: "USPS config is either invalid or not found."
    end
  end
end


Wrap System.cmd call o try rescue

Wrap System.cmd call o try rescue

defmodule Credo.BuildInfo do
  @moduledoc false

  # This is called at compile-time and should not be used at runtime!

  @doc false
  def version(mix_version) do
    version(mix_version, git_info())
  end

  defp version(mix_version, nil) do
    mix_version
  end

  defp version(mix_version, git_info) do
    version =
      if git_info.tag == "v#{mix_version}" do
        mix_version
      else
        branch = git_info.branch || "nobranch"
        commit = git_info.commit || "nocommit"

        "#{mix_version}-ref.#{branch}.#{commit}"
      end

    if git_info.dirty? do
      "#{version}+uncommittedchanges"
    else
      version
    end
  end

  defp git_info do
    if git_present?() do
      %{
        commit: git_commit(),
        branch: git_branch(),
        tag: git_tag(),
        dirty?: git_dirty?()
      }
    end
  end

  defp git_present? do
    try do
      {_, exit_status} = System.cmd("git", ["--help"])
      exit_status == 0
    rescue
      _ -> false
    end
  end

  defp git_branch do
    case System.cmd("git", ["branch"]) do
      {output, 0} -> git_branch(output)
      {_, _} -> nil
    end
  end

  defp git_branch(output) do
    line_with_active_branch =
      output
      |> String.split("\n")
      |> Enum.find(fn
        "* " <> _ -> true
        _ -> false
      end)

    case line_with_active_branch do
      "* (HEAD detached at origin/" <> remote_branch_name ->
        String.replace(remote_branch_name, ~r/\)$/, "")

      "* (HEAD detached at " <> branch_name ->
        String.replace(branch_name, ~r/\)$/, "")

      "* " <> branch_name ->
        branch_name

      _ ->
        nil
    end
  end

  defp git_commit do
    case System.cmd("git", ["rev-parse", "--short", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_tag do
    case System.cmd("git", ["tag", "--points-at", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_dirty? do
    case System.cmd("git", ["status", "--short"]) do
      {output, 0} -> output |> String.trim() |> git_dirty?()
      {_, _} -> nil
    end
  end

  defp git_dirty?(""), do: false
  # Hex puts a `.fetch` file in the working dir when downloading deps via git
  defp git_dirty?("?? .fetch"), do: false
  defp git_dirty?(_), do: true
end

defmodule Credo.BuildInfo do
  @moduledoc false

  # This is called at compile-time and should not be used at runtime!

  @doc false
  def version(mix_version) do
    version(mix_version, git_info())
  end

  defp version(mix_version, nil) do
    mix_version
  end

  defp version(mix_version, git_info) do
    version =
      if git_info.tag == "v#{mix_version}" do
        mix_version
      else
        branch = git_info.branch || "nobranch"
        commit = git_info.commit || "nocommit"

        "#{mix_version}-ref.#{branch}.#{commit}"
      end

    if git_info.dirty? do
      "#{version}+uncommittedchanges"
    else
      version
    end
  end

  defp git_info do
    if git_present?() do
      %{
        commit: git_commit(),
        branch: git_branch(),
        tag: git_tag(),
        dirty?: git_dirty?()
      }
    end
  end

  defp git_present? do
    {_, exit_status} = System.cmd("git", ["--help"])

    exit_status == 0
  end

  defp git_branch do
    case System.cmd("git", ["branch"]) do
      {output, 0} -> git_branch(output)
      {_, _} -> nil
    end
  end

  defp git_branch(output) do
    line_with_active_branch =
      output
      |> String.split("\n")
      |> Enum.find(fn
        "* " <> _ -> true
        _ -> false
      end)

    case line_with_active_branch do
      "* (HEAD detached at origin/" <> remote_branch_name ->
        String.replace(remote_branch_name, ~r/\)$/, "")

      "* (HEAD detached at " <> branch_name ->
        String.replace(branch_name, ~r/\)$/, "")

      "* " <> branch_name ->
        branch_name

      _ ->
        nil
    end
  end

  defp git_commit do
    case System.cmd("git", ["rev-parse", "--short", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_tag do
    case System.cmd("git", ["tag", "--points-at", "HEAD"]) do
      {output, 0} -> String.trim(output)
      {_, _} -> nil
    end
  end

  defp git_dirty? do
    case System.cmd("git", ["status", "--short"]) do
      {output, 0} -> output |> String.trim() |> git_dirty?()
      {_, _} -> nil
    end
  end

  defp git_dirty?(""), do: false
  # Hex puts a `.fetch` file in the working dir when downloading deps via git
  defp git_dirty?("?? .fetch"), do: false
  defp git_dirty?(_), do: true
end


Rename shift_{left,right} helper functions in Float

import Kernel, except: [round: 1]

defmodule Float do
  @moduledoc """
  Functions for working with floating point numbers.
  """

  import Bitwise

  @power_of_2_to_52 4503599627370496

  @doc """
  Parses a binary into a float.

  If successful, returns a tuple in the form of `{float, remainder_of_binary}`;
  when the binary cannot be coerced into a valid float, the atom `:error` is
  returned.

  If the size of float exceeds the maximum size of `1.7976931348623157e+308`,
  the `ArgumentError` exception is raised.

  If you want to convert a string-formatted float directly to a float,
  `String.to_float/1` can be used instead.

  ## Examples

      iex> Float.parse("34")
      {34.0, ""}
      iex> Float.parse("34.25")
      {34.25, ""}
      iex> Float.parse("56.5xyz")
      {56.5, "xyz"}

      iex> Float.parse("pi")
      :error

  """
  @spec parse(binary) :: {float, binary} | :error
  def parse("-" <> binary) do
    case parse_unsigned(binary) do
      :error -> :error
      {number, remainder} -> {-number, remainder}
    end
  end

  def parse("+" <> binary) do
    parse_unsigned(binary)
  end

  def parse(binary) do
    parse_unsigned(binary)
  end

  defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9, do:
    parse_unsigned(rest, false, false, <<digit>>)

  defp parse_unsigned(binary) when is_binary(binary), do:
    :error

  defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9, do:
    parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)

  defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9, do:
    parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)

  defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc) when exp_marker in 'eE' and  digit in ?0..?9, do:
    parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)

  defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc) when exp_marker in 'eE' and sign in '-+' and digit in ?0..?9, do:
    parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)

  defp parse_unsigned(rest, dot?, _e?, acc), do:
    {:erlang.binary_to_float(add_dot(acc, dot?)), rest}

  defp add_dot(acc, true),  do: acc
  defp add_dot(acc, false), do: acc <> ".0"

  @doc """
  Rounds a float to the largest integer less than or equal to `num`.

  `floor/2` also accepts a precision to round a floating point value down
  to an arbitrary number of fractional digits (between 0 and 15).
  The operation is performed on the binary floating point, without a
  conversion to decimal.

  The behaviour of `floor/2` for floats can be surprising. For example:

      iex> Float.floor(12.52, 2)
      12.51

  One may have expected it to floor to 12.52. This is not a bug.
  Most decimal fractions cannot be represented as a binary floating point
  and therefore the number above is internally represented as 12.51999999,
  which explains the behaviour above.

  This function always returns a float. `Kernel.trunc/1` may be used instead to
  truncate the result to an integer afterwards.

  ## Examples

      iex> Float.floor(34.25)
      34.0
      iex> Float.floor(-56.5)
      -57.0
      iex> Float.floor(34.259, 2)
      34.25

  """
  @spec floor(float, 0..15) :: float
  def floor(number, precision \\ 0) when is_float(number) and precision in 0..15 do
    round(number, precision, :floor)
  end

  @doc """
  Rounds a float to the smallest integer greater than or equal to `num`.

  `ceil/2` also accepts a precision to round a floating point value down
  to an arbitrary number of fractional digits (between 0 and 15).

  The operation is performed on the binary floating point, without a
  conversion to decimal.

  The behaviour of `ceil/2` for floats can be surprising. For example:

      iex> Float.ceil(-12.52, 2)
      -12.51

  One may have expected it to ceil to -12.52. This is not a bug.
  Most decimal fractions cannot be represented as a binary floating point
  and therefore the number above is internally represented as -12.51999999,
  which explains the behaviour above.

  This function always returns floats. `Kernel.trunc/1` may be used instead to
  truncate the result to an integer afterwards.

  ## Examples

      iex> Float.ceil(34.25)
      35.0
      iex> Float.ceil(-56.5)
      -56.0
      iex> Float.ceil(34.251, 2)
      34.26

  """
  @spec ceil(float, 0..15) :: float
  def ceil(number, precision \\ 0) when is_float(number) and precision in 0..15 do
    round(number, precision, :ceil)
  end

  @doc """
  Rounds a floating point value to an arbitrary number of fractional
  digits (between 0 and 15).

  The rounding direction always ties to half up. The operation is
  performed on the binary floating point, without a conversion to decimal.

  This function only accepts floats and always returns a float. Use
  `Kernel.round/1` if you want a function that accepts both floats
  and integers and always returns an integer.

  The behaviour of `round/2` for floats can be surprising. For example:

      iex> Float.round(5.5675, 3)
      5.567

  One may have expected it to round to the half up 5.568. This is not a bug.
  Most decimal fractions cannot be represented as a binary floating point
  and therefore the number above is internally represented as 5.567499999,
  which explains the behaviour above. If you want exact rounding for decimals,
  you must use a decimal library. The behaviour above is also in accordance
  to reference implementations, such as "Correctly Rounded Binary-Decimal and
  Decimal-Binary Conversions" by David M. Gay.

  ## Examples

      iex> Float.round(12.5)
      13.0
      iex> Float.round(5.5674, 3)
      5.567
      iex> Float.round(5.5675, 3)
      5.567
      iex> Float.round(-5.5674, 3)
      -5.567
      iex> Float.round(-5.5675)
      -6.0
      iex> Float.round(12.341444444444441, 15)
      12.341444444444441

  """
  @spec round(float, 0..15) :: float

  # This implementation is slow since it relies on big integers.
  # Faster implementations are available on more recent papers
  # and could be implemented in the future.
  def round(float, precision \\ 0) when is_float(float) and precision in 0..15 do
    round(float, precision, :half_up)
  end

  defp round(float, precision, rounding) do
    <<sign::size(1), exp::size(11), significant::size(52)-bitstring>> = <<float::float>>
    {num, count, _} = decompose(significant)
    count = count - exp + 1023

    cond do
      count <= 0 -> # There is no decimal precision
        float

      count >= 104 -> # Precision beyond 15 digits
        case rounding do
          :ceil when sign === 0 -> 1 / power_of_10(precision)
          :floor when sign === 1 -> -1 / power_of_10(precision)
          _ -> 0.0
        end

      count <= precision -> # We are asking more precision than we have
        float

      true ->
        # Difference in precision between float and asked precision
        # We subtract 1 because we need to calculate the remainder too
        diff = count - precision - 1

        # Get up to latest so we calculate the remainder
        power_of_10 = power_of_10(diff)

        # Convert the numerand to decimal base
        num = num * power_of_5(count)

        # Move to the given precision - 1
        num = div(num, power_of_10)
        div = div(num, 10)
        num = rounding(rounding, sign, num, div)

        # Convert back to float without loss
        # http://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
        den = power_of_10(precision)
        boundary = den <<< 52

        cond do
          num == 0 ->
            0.0
          num >= boundary ->
            {den, exp} = scale_down(num, boundary, 52)
            decimal_to_float(sign, num, den, exp)
          true ->
            {num, exp} = scale_up(num, boundary, 52)
            decimal_to_float(sign, num, den, exp)
        end
    end
  end

  defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
  defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)

  defp scale_down(num, den, exp) do
    new_den = den <<< 1
    if num < new_den do
      {den >>> 52, exp}
    else
      scale_down(num, new_den, exp + 1)
    end
  end

  defp decimal_to_float(sign, num, den, exp) do
    quo = div(num, den)
    rem = num - quo * den

    tmp =
      case den >>> 1 do
        den when rem > den -> quo + 1
        den when rem < den -> quo
        _ when (quo &&& 1) === 1 -> quo + 1
        _ -> quo
      end

    tmp = tmp - @power_of_2_to_52
    <<tmp::float>> = <<sign::size(1), (exp + 1023)::size(11), tmp::size(52)>>
    tmp
  end

  defp rounding(:floor, 1, _num, div), do: div + 1
  defp rounding(:ceil, 0, _num, div), do: div + 1
  defp rounding(:half_up, _sign, num, div) do
    case rem(num, 10) do
      rem when rem < 5 -> div
      rem when rem >= 5 -> div + 1
    end
  end
  defp rounding(_, _, _, div), do: div

  Enum.reduce 0..104, 1, fn x, acc ->
    defp power_of_10(unquote(x)), do: unquote(acc)
    acc * 10
  end

  Enum.reduce 0..104, 1, fn x, acc ->
    defp power_of_5(unquote(x)), do: unquote(acc)
    acc * 5
  end

  @doc """
  Returns a pair of integers whose ratio is exactly equal
  to the original float and with a positive denominator.

  ## Examples

      iex> Float.ratio(3.14)
      {7070651414971679, 2251799813685248}
      iex> Float.ratio(-3.14)
      {-7070651414971679, 2251799813685248}
      iex> Float.ratio(1.5)
      {3, 2}
      iex> Float.ratio(-1.5)
      {-3, 2}
      iex> Float.ratio(16.0)
      {16, 1}
      iex> Float.ratio(-16.0)
      {-16, 1}

  """
  def ratio(float) when is_float(float) do
    <<sign::size(1), exp::size(11), significant::size(52)-bitstring>> = <<float::float>>
    {num, _, den} = decompose(significant)
    num = sign(sign, num)
    case exp - 1023 do
      exp when exp > 0 ->
        {den, exp} = shift_right(den, exp)
        {shift_left(num, exp), den}
      exp when exp < 0 ->
        {num, shift_left(den, -exp)}
      0 ->
        {num, den}
    end
  end

  defp decompose(significant) do
    decompose(significant, 1, 0, 2, 1, 1)
  end

  defp decompose(<<1::size(1), bits::bitstring>>, count, last_count, power, _last_power, acc) do
    decompose(bits, count + 1, count, power <<< 1, power, shift_left(acc, count - last_count) + 1)
  end
  defp decompose(<<0::size(1), bits::bitstring>>, count, last_count, power, last_power, acc) do
    decompose(bits, count + 1, last_count, power <<< 1, last_power, acc)
  end
  defp decompose(<<>>, _count, last_count, _power, last_power, acc) do
    {acc, last_count, last_power}
  end

  defp sign(0, num), do: num
  defp sign(1, num), do: -num

  defp shift_left(num, 0), do: num
  defp shift_left(num, times), do: shift_left(num <<< 1, times - 1)

  defp shift_right(num, 0), do: {num, 0}
  defp shift_right(1, times), do: {1, times}
  defp shift_right(num, times), do: shift_right(num >>> 1, times - 1)

  @doc """
  Returns a charlist which corresponds to the text representation
  of the given float.

  It uses the shortest representation according to algorithm described
  in "Printing Floating-Point Numbers Quickly and Accurately" in
  Proceedings of the SIGPLAN '96 Conference on Programming Language
  Design and Implementation.

  ## Examples

      iex> Float.to_charlist(7.0)
      '7.0'

  """
  @spec to_charlist(float) :: charlist
  def to_charlist(float) when is_float(float) do
    :io_lib_format.fwrite_g(float)
  end

  @doc """
  Returns a binary which corresponds to the text representation
  of the given float.

  It uses the shortest representation according to algorithm described
  in "Printing Floating-Point Numbers Quickly and Accurately" in
  Proceedings of the SIGPLAN '96 Conference on Programming Language
  Design and Implementation.

  ## Examples

      iex> Float.to_string(7.0)
      "7.0"

  """
  @spec to_string(float) :: String.t
  def to_string(float) when is_float(float) do
    IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
  end

  # TODO: Deprecate by v1.5
  @doc false
  def to_char_list(float), do: Float.to_charlist(float)

  @doc false
  def to_char_list(float, options) do
    IO.warn "Float.to_char_list/2 is deprecated, use :erlang.float_to_list/2 instead"
    :erlang.float_to_list(float, expand_compact(options))
  end

  @doc false
  def to_string(float, options) do
    IO.warn "Float.to_string/2 is deprecated, use :erlang.float_to_binary/2 instead"
    :erlang.float_to_binary(float, expand_compact(options))
  end

  defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
  defp expand_compact([{:compact, true} | t]),  do: [:compact | expand_compact(t)]
  defp expand_compact([h | t]),                 do: [h | expand_compact(t)]
  defp expand_compact([]),                      do: []
end

import Kernel, except: [round: 1]

defmodule Float do
  @moduledoc """
  Functions for working with floating point numbers.
  """

  import Bitwise

  @power_of_2_to_52 4503599627370496

  @doc """
  Parses a binary into a float.

  If successful, returns a tuple in the form of `{float, remainder_of_binary}`;
  when the binary cannot be coerced into a valid float, the atom `:error` is
  returned.

  If the size of float exceeds the maximum size of `1.7976931348623157e+308`,
  the `ArgumentError` exception is raised.

  If you want to convert a string-formatted float directly to a float,
  `String.to_float/1` can be used instead.

  ## Examples

      iex> Float.parse("34")
      {34.0, ""}
      iex> Float.parse("34.25")
      {34.25, ""}
      iex> Float.parse("56.5xyz")
      {56.5, "xyz"}

      iex> Float.parse("pi")
      :error

  """
  @spec parse(binary) :: {float, binary} | :error
  def parse("-" <> binary) do
    case parse_unsigned(binary) do
      :error -> :error
      {number, remainder} -> {-number, remainder}
    end
  end

  def parse("+" <> binary) do
    parse_unsigned(binary)
  end

  def parse(binary) do
    parse_unsigned(binary)
  end

  defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9, do:
    parse_unsigned(rest, false, false, <<digit>>)

  defp parse_unsigned(binary) when is_binary(binary), do:
    :error

  defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9, do:
    parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)

  defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9, do:
    parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)

  defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc) when exp_marker in 'eE' and  digit in ?0..?9, do:
    parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)

  defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc) when exp_marker in 'eE' and sign in '-+' and digit in ?0..?9, do:
    parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)

  defp parse_unsigned(rest, dot?, _e?, acc), do:
    {:erlang.binary_to_float(add_dot(acc, dot?)), rest}

  defp add_dot(acc, true),  do: acc
  defp add_dot(acc, false), do: acc <> ".0"

  @doc """
  Rounds a float to the largest integer less than or equal to `num`.

  `floor/2` also accepts a precision to round a floating point value down
  to an arbitrary number of fractional digits (between 0 and 15).
  The operation is performed on the binary floating point, without a
  conversion to decimal.

  The behaviour of `floor/2` for floats can be surprising. For example:

      iex> Float.floor(12.52, 2)
      12.51

  One may have expected it to floor to 12.52. This is not a bug.
  Most decimal fractions cannot be represented as a binary floating point
  and therefore the number above is internally represented as 12.51999999,
  which explains the behaviour above.

  This function always returns a float. `Kernel.trunc/1` may be used instead to
  truncate the result to an integer afterwards.

  ## Examples

      iex> Float.floor(34.25)
      34.0
      iex> Float.floor(-56.5)
      -57.0
      iex> Float.floor(34.259, 2)
      34.25

  """
  @spec floor(float, 0..15) :: float
  def floor(number, precision \\ 0) when is_float(number) and precision in 0..15 do
    round(number, precision, :floor)
  end

  @doc """
  Rounds a float to the smallest integer greater than or equal to `num`.

  `ceil/2` also accepts a precision to round a floating point value down
  to an arbitrary number of fractional digits (between 0 and 15).

  The operation is performed on the binary floating point, without a
  conversion to decimal.

  The behaviour of `ceil/2` for floats can be surprising. For example:

      iex> Float.ceil(-12.52, 2)
      -12.51

  One may have expected it to ceil to -12.52. This is not a bug.
  Most decimal fractions cannot be represented as a binary floating point
  and therefore the number above is internally represented as -12.51999999,
  which explains the behaviour above.

  This function always returns floats. `Kernel.trunc/1` may be used instead to
  truncate the result to an integer afterwards.

  ## Examples

      iex> Float.ceil(34.25)
      35.0
      iex> Float.ceil(-56.5)
      -56.0
      iex> Float.ceil(34.251, 2)
      34.26

  """
  @spec ceil(float, 0..15) :: float
  def ceil(number, precision \\ 0) when is_float(number) and precision in 0..15 do
    round(number, precision, :ceil)
  end

  @doc """
  Rounds a floating point value to an arbitrary number of fractional
  digits (between 0 and 15).

  The rounding direction always ties to half up. The operation is
  performed on the binary floating point, without a conversion to decimal.

  This function only accepts floats and always returns a float. Use
  `Kernel.round/1` if you want a function that accepts both floats
  and integers and always returns an integer.

  The behaviour of `round/2` for floats can be surprising. For example:

      iex> Float.round(5.5675, 3)
      5.567

  One may have expected it to round to the half up 5.568. This is not a bug.
  Most decimal fractions cannot be represented as a binary floating point
  and therefore the number above is internally represented as 5.567499999,
  which explains the behaviour above. If you want exact rounding for decimals,
  you must use a decimal library. The behaviour above is also in accordance
  to reference implementations, such as "Correctly Rounded Binary-Decimal and
  Decimal-Binary Conversions" by David M. Gay.

  ## Examples

      iex> Float.round(12.5)
      13.0
      iex> Float.round(5.5674, 3)
      5.567
      iex> Float.round(5.5675, 3)
      5.567
      iex> Float.round(-5.5674, 3)
      -5.567
      iex> Float.round(-5.5675)
      -6.0
      iex> Float.round(12.341444444444441, 15)
      12.341444444444441

  """
  @spec round(float, 0..15) :: float

  # This implementation is slow since it relies on big integers.
  # Faster implementations are available on more recent papers
  # and could be implemented in the future.
  def round(float, precision \\ 0) when is_float(float) and precision in 0..15 do
    round(float, precision, :half_up)
  end

  defp round(float, precision, rounding) do
    <<sign::size(1), exp::size(11), significant::size(52)-bitstring>> = <<float::float>>
    {num, count, _} = decompose(significant)
    count = count - exp + 1023

    cond do
      count <= 0 -> # There is no decimal precision
        float

      count >= 104 -> # Precision beyond 15 digits
        case rounding do
          :ceil when sign === 0 -> 1 / power_of_10(precision)
          :floor when sign === 1 -> -1 / power_of_10(precision)
          _ -> 0.0
        end

      count <= precision -> # We are asking more precision than we have
        float

      true ->
        # Difference in precision between float and asked precision
        # We subtract 1 because we need to calculate the remainder too
        diff = count - precision - 1

        # Get up to latest so we calculate the remainder
        power_of_10 = power_of_10(diff)

        # Convert the numerand to decimal base
        num = num * power_of_5(count)

        # Move to the given precision - 1
        num = div(num, power_of_10)
        div = div(num, 10)
        num = rounding(rounding, sign, num, div)

        # Convert back to float without loss
        # http://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
        den = power_of_10(precision)
        boundary = den <<< 52

        cond do
          num == 0 ->
            0.0
          num >= boundary ->
            {den, exp} = scale_down(num, boundary, 52)
            decimal_to_float(sign, num, den, exp)
          true ->
            {num, exp} = scale_up(num, boundary, 52)
            decimal_to_float(sign, num, den, exp)
        end
    end
  end

  defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
  defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)

  defp scale_down(num, den, exp) do
    new_den = den <<< 1
    if num < new_den do
      {den >>> 52, exp}
    else
      scale_down(num, new_den, exp + 1)
    end
  end

  defp decimal_to_float(sign, num, den, exp) do
    quo = div(num, den)
    rem = num - quo * den

    tmp =
      case den >>> 1 do
        den when rem > den -> quo + 1
        den when rem < den -> quo
        _ when (quo &&& 1) === 1 -> quo + 1
        _ -> quo
      end

    tmp = tmp - @power_of_2_to_52
    <<tmp::float>> = <<sign::size(1), (exp + 1023)::size(11), tmp::size(52)>>
    tmp
  end

  defp rounding(:floor, 1, _num, div), do: div + 1
  defp rounding(:ceil, 0, _num, div), do: div + 1
  defp rounding(:half_up, _sign, num, div) do
    case rem(num, 10) do
      rem when rem < 5 -> div
      rem when rem >= 5 -> div + 1
    end
  end
  defp rounding(_, _, _, div), do: div

  Enum.reduce 0..104, 1, fn x, acc ->
    defp power_of_10(unquote(x)), do: unquote(acc)
    acc * 10
  end

  Enum.reduce 0..104, 1, fn x, acc ->
    defp power_of_5(unquote(x)), do: unquote(acc)
    acc * 5
  end

  @doc """
  Returns a pair of integers whose ratio is exactly equal
  to the original float and with a positive denominator.

  ## Examples

      iex> Float.ratio(3.14)
      {7070651414971679, 2251799813685248}
      iex> Float.ratio(-3.14)
      {-7070651414971679, 2251799813685248}
      iex> Float.ratio(1.5)
      {3, 2}
      iex> Float.ratio(-1.5)
      {-3, 2}
      iex> Float.ratio(16.0)
      {16, 1}
      iex> Float.ratio(-16.0)
      {-16, 1}

  """
  def ratio(float) when is_float(float) do
    <<sign::size(1), exp::size(11), significant::size(52)-bitstring>> = <<float::float>>
    {num, _, den} = decompose(significant)
    num = sign(sign, num)
    case exp - 1023 do
      exp when exp > 0 ->
        {den, exp} = shift_right_until_zero(den, exp)
        {shift_left_until_zero(num, exp), den}
      exp when exp < 0 ->
        {num, shift_left_until_zero(den, abs(exp))}
      0 ->
        {num, den}
    end
  end

  defp decompose(significant) do
    decompose(significant, 1, 0, 2, 1, 1)
  end

  defp decompose(<<1::size(1), bits::bitstring>>, count, last_count, power, _last_power, acc) do
    decompose(bits, count + 1, count, power <<< 1, power, shift_left_until_zero(acc, count - last_count) + 1)
  end
  defp decompose(<<0::size(1), bits::bitstring>>, count, last_count, power, last_power, acc) do
    decompose(bits, count + 1, last_count, power <<< 1, last_power, acc)
  end
  defp decompose(<<>>, _count, last_count, _power, last_power, acc) do
    {acc, last_count, last_power}
  end

  defp sign(0, num), do: num
  defp sign(1, num), do: -num

  defp shift_left_until_zero(num, 0), do: num
  defp shift_left_until_zero(num, x), do: shift_left_until_zero(num <<< 1, x - 1)

  defp shift_right_until_zero(num, 0), do: {num, 0}
  defp shift_right_until_zero(1, x),   do: {1, x}
  defp shift_right_until_zero(num, x), do: shift_right_until_zero(num >>> 1, x - 1)

  @doc """
  Returns a charlist which corresponds to the text representation
  of the given float.

  It uses the shortest representation according to algorithm described
  in "Printing Floating-Point Numbers Quickly and Accurately" in
  Proceedings of the SIGPLAN '96 Conference on Programming Language
  Design and Implementation.

  ## Examples

      iex> Float.to_charlist(7.0)
      '7.0'

  """
  @spec to_charlist(float) :: charlist
  def to_charlist(float) when is_float(float) do
    :io_lib_format.fwrite_g(float)
  end

  @doc """
  Returns a binary which corresponds to the text representation
  of the given float.

  It uses the shortest representation according to algorithm described
  in "Printing Floating-Point Numbers Quickly and Accurately" in
  Proceedings of the SIGPLAN '96 Conference on Programming Language
  Design and Implementation.

  ## Examples

      iex> Float.to_string(7.0)
      "7.0"

  """
  @spec to_string(float) :: String.t
  def to_string(float) when is_float(float) do
    IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
  end

  # TODO: Deprecate by v1.5
  @doc false
  def to_char_list(float), do: Float.to_charlist(float)

  @doc false
  def to_char_list(float, options) do
    IO.warn "Float.to_char_list/2 is deprecated, use :erlang.float_to_list/2 instead"
    :erlang.float_to_list(float, expand_compact(options))
  end

  @doc false
  def to_string(float, options) do
    IO.warn "Float.to_string/2 is deprecated, use :erlang.float_to_binary/2 instead"
    :erlang.float_to_binary(float, expand_compact(options))
  end

  defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
  defp expand_compact([{:compact, true} | t]),  do: [:compact | expand_compact(t)]
  defp expand_compact([h | t]),                 do: [h | expand_compact(t)]
  defp expand_compact([]),                      do: []
end