Elixir:小数转换并且不允许负数

Elixir: Decimal cast and dont allow negative numbers

你好,我收到一个值,这个值不能为负,只能为正,我想 return 如果它是负的则自定义错误,如果它是负的则继续处理管道。

我现在有这个:

 def call(%{"id" => id, "value" => value}, operation) do
    Multi.new()
    |> Multi.run(:account, fn repo, _changes -> get_account(repo, id) end)
    |> Multi.run(:update_balance, fn repo, %{account: account} ->
      update_balance(repo, account, value, operation)
    end)
  end
  defp update_balance(repo, account, value, operation) do
    account
    |> operation(value, operation)
    |> update_account(repo, account)
  end
  defp operation(%Account{balance: balance}, value, operation) do
    value
    |> Decimal.cast()
    |> handle_cast(balance, operation)
  end

  defp handle_cast({:ok, value}, balance, :deposit), do: Decimal.add(balance, value)
  defp handle_cast({:ok, value}, balance, :withdraw), do: Decimal.sub(balance, value)
  defp handle_cast(:error, _balance, _operation), do: {:error, "Invalid operation!"}

正如我在上一条评论中提到的,Decimal 是一种不同的“类型”,它大于整数。

幸运的是,Decimal 提供了 gt?lt?compare 函数,您可以使用这些函数将 Decimal 与整数进行比较。一个稍微干净的解决方案,尽可能少地改变你的代码,是在 handle_cast 之前添加一个小的额外函数和 handle_cast 的另一个定义。像这样:

defp operation(%Account{balance: balance}, value, operation) do
  value
  |> Decimal.cast()
  |> maybe_positive?()
  |> handle_cast(balance, operation)
end

defp maybe_positive?({:ok, value}) do
  case Decimal.compare?(value, 0) do
    :lt -> {:error, :negative_number}
    :gt -> {:ok, value}
  end
end

defp maybe_positive?(error), do: error

defp handle_cast({:error, :negative_number}, _balance, _operation) do
  {:error, "Number must be positive"}
end
defp handle_cast({:ok, value}, balance, :deposit), do: Decimal.add(balance, value)
...

这样,如果它是负数,它将通过 handle_cast{:error, :negative_number} 案例,如果它是正数或 cast 由于某种原因失败,它将通过您已经拥有的其他 handle_cast 个验证

虽然@sbacarob 的回答完全正确,但为了方便起见,我会post这样做。

Decimal 是一个 plain struct,因此可以使用它的内部结构来处理守卫的底片。这将适用于 Elixir ≥ 1.11 和 OTP ≥ 23 的现代版本。

defguard is_decimal_positive(value)
    when is_map(value) and
         value.__struct__ == Decimal and 
         value.sign == 1

有关详细信息,请参阅 Kernel.defguard/1。并将其用作

defp operation(%Account{balance: balance}, value, operation) do
  value
  |> Decimal.cast()
  |> handle_cast(balance, operation)
end

defp handle_cast({:ok, value}, balance, _) 
    when is_decimal_positive(value),
  do: Decimal.add(balance, value)

defp handle_cast(_error, _balance, _),
  do: {:error, "Number must be positive decimal"}

他也在做 ELixir NLW! 我是这样做的并且有效:

存储库:https://github.com/librity/nlw_elixir

defmodule Rocketpay.Accounts.Operation do
  alias Ecto.Multi
  alias Rocketpay.Account

  def call(%{"id" => id, "value" => value}, operation) do
    fetch_account_operation_name = build_fetch_account_operation_name(operation)

    Multi.new()
    |> Multi.run(
      fetch_account_operation_name,
      fn repo, _previous -> fetch_account(repo, id) end
    )
    |> Multi.run(operation, fn repo, fetch_result ->
      account = Map.get(fetch_result, fetch_account_operation_name)

      update_balance(repo, account, value, operation)
    end)
  end

  defp fetch_account(repo, id) do
    case repo.get(Account, id) do
      nil -> {:error, "Account not found."}
      account -> {:ok, account}
    end
  end

  defp update_balance(repo, account, value, operation) do
    account
    |> calculate_new_balance(value, operation)
    |> update_account(account, repo)
  end

  defp calculate_new_balance(%Account{balance: balance}, value, operation) do
    value
    |> Decimal.cast()
    |> validate_greater_than_zero()
    |> handle_cast(balance, operation)
  end

  defp validate_greater_than_zero({:ok, casted_value}) do
    case Decimal.compare(casted_value, 0) do
      :lt -> :error
      :eq -> :error
      :gt -> {:ok, casted_value}
    end
  end

  defp validate_greater_than_zero(:error), do: :error

  defp handle_cast({:ok, valid_value}, balance, :deposit), do: Decimal.add(balance, valid_value)
  defp handle_cast({:ok, valid_value}, balance, :withdraw), do: Decimal.sub(balance, valid_value)

  defp handle_cast(:error, _balance, :deposit), do: {:error, "Invalid deposit value."}
  defp handle_cast(:error, _balance, :withdraw), do: {:error, "Invalid withdraw value."}

  defp update_account({:error, _reason} = error, _account, _repo), do: error

  defp update_account(new_balance, account, repo) do
    account
    |> Account.changeset(%{balance: new_balance})
    |> repo.update()
  end

  defp build_fetch_account_operation_name(operation),
    do: "fetch_#{operation}_account" |> String.to_atom()
end