0

I am trying to convert the price field, which is a string (eg "2.22" or "") to a float or nil, and then add it to the database.

  def insert_product_shop(conn, product_id, shop_id, price) do
    priceFloat = nil
    if price not in [""] do
      price = elem(Float.parse(price), 0)
      priceFloat = price / 1
      IO.inspect(priceFloat)
    else
      priceFloat = nil
    end
    IO.inspect(priceFloat)
    changeset = Api.ProductShop.changeset(%Api.ProductShop{
      p_id: product_id,
      s_id: shop_id,
      price: priceFloat,
      not_in_shop_count: 0,
      is_in_shop_count: 0
      })
    errors = changeset.errors
    valid = changeset.valid?
    IO.inspect(changeset)
    case insert(changeset) do
      {:ok, product_shop} ->
        {:ok, product_shop}
      {:error, changeset} ->
        {:error, :failure}
    end
  end

the output is:

2.22
nil
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Api.ProductShop<>,
 valid?: true>

13:25:41.745 [debug] QUERY OK db=2.0ms
INSERT INTO "product_shops" ("is_in_shop_count","not_in_shop_count","p_id","s_id") VALUES ($1,$2,$3,$4) RETURNING "id" [0, 0, 40, 1]

As the output shows, priceFloat becomes nil, I assume because when I set it to 2.22 it was out of scope. Maybe my code is too imperative. How can I rewrite this to convert "2.22" to 2.22 without making it nil, and allow "" to be converted to nil?

4 Answers 4

2

As the output shows, priceFloat becomes nil, I assume because when I set it to 2.22 it was out of scope.

Almost right. Rather that the variable you are trying to set being out of scope, the problem is that the variable you assign to inside the if statement goes out of scope. It just happens to have the same name as the variable outside the if statement.

The solution is to assign the result of the if/else statement to the variable. Here is your code with minimal changes:

price = "2.22"

priceFloat =
  if price not in [""] do
    elem(Float.parse(price), 0)
  else
    nil
  end

IO.inspect(priceFloat)

However, it's still not very idiomatic. You can take advantage of the fact that Float.parse/1 returns :error when the input is the empty string to write it like with a case expression:

priceFloat =
  case Float.parse(price) do
    {float, ""} -> float
    :error -> nil
  end
Sign up to request clarification or add additional context in comments.

4 Comments

Blind elem/2 is potentially dangerous because it’d happily deal with Float.parse("3.14some❤❤❤garbage").
That's why I suggested matching against {float, ""}.
I know :) I just pointed out that you’d probably better mention this (it’s not only counter-idiomatic, it’s somewhat dangerous.)
Good point, I should have explained that explicitly in the answer, but now you did it for me ;-)
1

You can use case to evaluate the returned value by Float.parse and assign nil when it returns :error, assuming that the purpose of your if is to avoid the parsing error

def insert_product_shop(conn, product_id, shop_id, price) do
  priceFloat = case Float.parse(price) do
    {value, _remainder} -> value
    :error -> nil
  end
  ...
end

3 Comments

This code is potentially dangerous because it’d happily accept Float.parse("3.14some❤❤❤garbage").
@AlekseiMatiushkin really? but the garbage would be left in the remainder, would't it? how could it be dangerous? I appreciate the comment BTW, it's always good to learn this stuff
The garbage would indeed be left in the remainder, but 1.0a being successfully parsed as float, and a1.0 returning the error does not seem to be super-consistent :)
1

You can use a combination of pattern matching and method overloading to solve the problem:

defmodule Example do
  def parsePrice(""), do: nil
  def parsePrice(price) when is_float(price), do: price
  def parsePrice(price) when is_binary(price) do
    {float, _} = Float.parse(price)
    float
  end
end

Example.parsePrice(2.22) |> IO.inspect
Example.parsePrice("2.22") |> IO.inspect

(The equivalent is achievable using a case statement)

If you pass anything that is not a binary (a string) or a float to this function it will cause a pattern unmatched error. This may be good in case you have some error reporting in place, so you can detect unexpected usage of your code.

For a better debugging experience, I encourage you to use the built-in debugger via IEx.pry/0.

3 Comments

Thanks! I get an error when I pass in "": (MatchError) no match of right hand side value: :error. I would like to return nil under the scenario that price is "".
{float, _} pattern match is potentially dangerous because it’d happily match Float.parse("3.14some❤❤❤garbage").
Also, def parsePrice(""), do: nil clause is redundant, an empty string would be ruled out by Float.parse/1 on its own.
1

For the sake of diversity, I’d post another approach that uses with/1 special form.

with {f, ""} <- Float.parse("3.14"),
  do: f,
  else: (_ -> nil)

Here we explicitly match the float only. Any trailing garbage would be discarded. If the match succeeds, we return the float, otherwise, we return nil.


Beware of Float.parse/1 might be confused by garbage that looks like scientific notation.

(with {f, ""} <- Float.parse("3e14"), do: f) == 300_000_000_000_000
#⇒ true

Important sidenote: assigning priceFloat inside if does not change the value of the priceFloat variable outside of the scope. Scoping in is pretty important and one cannot propagate local variables to the outermost scope, unlike most of the languages.

foo = 42
if true, do: foo = 3.14
IO.puts(foo)
#⇒ 42

Well, to some extent it’s possible to affect outermost scope variables from macros with var!/2, and if is indeed a macro, but this whole stuff is definitely far beyond the scope of this question.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.