Validate custom changeset

Hey, guys. If anyone knows, I’d appreciate the help

I need a user to enter 3 numbers but I need to validate the following, which I don’t how to:

    • The sum of these 3 numbers must be 100
    • The numbers must be positive

This is what I have at the moment and I’m totally lost. I’ve done a lot of research but nothing enlights me.

My schema:

alias Numbers, as: N

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "transportation_schemas" do
    field :using_own, :decimal, default: 0
    field :using_provider, :decimal, default: 0
    field :using_third, :decimal, default: 0

    timestamps()
  end

My changeset:

def changeset(transportation_schema, attrs) do
    transportation_schema
    |> cast(attrs, [:using_own, :using_provider, :using_third])
    |> validate_required([:using_own, :using_provider, :using_third])
    |> validate_percentage()
  end

And this is the function that validates the sum:

defp validate_percentage(changeset) do
    using_own = get_field(changeset, :using_own)
    using_provider = get_field(changeset, :using_provider)
    using_third = get_field(changeset, :using_third)

    sum = N.add(using_own, using_provider) |> N.add(using_third)

    IO.puts(sum)

    if sum < 100 do
      add_error(changeset, :using_own, "less")
    end
  end

I understand that the line: add_error(changeset, :using_own, "less") is totally wrong because I don’t need a field but the result of a sum. I don’t know if add_error even solves the problem.

I just looking for something like “Error. Your values don’t sum 100”

And as far the positive numbers I have no idea how to

Hopefully anyone as done something similar

I would start by separating the concern… one is to check if it is a positive number, one for the sum.

The first should be easy, but I would pass the field as a parameter, to reuse this validation multiple time.

Something like

def validate_is_positive(changeset, field) do

end

I would also consider validate_range if it is… a range.

For the sum, You might add a custom error, but I would probably add 3 errors instead. For each field concerned by the sum. Like The sum of a, b, c must equal to 100.

2 Likes

Could a virtual field be defined just to be used for the sum validation add_error?

2 Likes

It is what I meant with custom error, but was not very clear I guess :slight_smile:

An error that would not be related to any of the 3 fields.

1 Like

“Wrong” depends on what the desired result is; if the application displays errors back to the user per-field, this will ensure the message is attached to a visible field.

You may also want to consider a different UX: three fields that must sum to 1.0 are really only two independent values. For instance, entering using_provider and useing_third is enough to calculate using_own - whether that calculation makes sense and/or is logical depends on your domain.

Beware comparing Decimal structs with numeric literals - this does not do what you want!

2 Likes

Thanks you very much, guys for your time. As far as the positivity of numbers I validated it this way:

    |> validate_number(:using_own, greater_than_or_equal_to: 0)
    |> validate_number(:using_provider, greater_than_or_equal_to: 0)
    |> validate_number(:using_third, greater_than_or_equal_to: 0)

I tried to put it all in one line using it in an array like |> validate_number([:using_own, :using_provider, :using_third], greater_than_or_equal_to: 0) but validate_number expects an atom as stated in its spec @spec validate_number(t(), atom(), Keyword.t()) :: t()

Don’t forget that currently your if statement returns nil at the moment (and so does the whole custom validation), you will want else changeset end

1 Like

Also, I just verified, add_error can take any atom as the key paramer, it doesn’t check if it’s a key in your struct. In your case you might want to call it :sum

1 Like

I tried that idea adding a virtual field like this:

    field :sum, :decimal, virtual: true

and using this function:

defp validate_sum_100(changeset) do
    using_own = get_field(changeset, :using_own)
    using_provider = get_field(changeset, :using_provider)
    using_third = get_field(changeset, :using_third)

    sum = using_own |> N.add(using_provider) |> N.add(using_third)

    put_change(changeset, :sum, sum)

    validate_number(changeset, :sum, equal_to: 100)
  end

But it doesn’t change anything because :sum doesn’t get any value assigned.

Definitely I’m doing something wrong there

The idea was using it just for the validation error message, something like:

defp validate_sum_100(changeset) do
    using_own = get_field(changeset, :using_own)
    using_provider = get_field(changeset, :using_provider)
    using_third = get_field(changeset, :using_third)

    sum = using_own |> N.add(using_provider) |> N.add(using_third)

    if N.equal?(sum, 100) do
       changeset
    else
      add_error(changeset, :sum, "Must equal to 100")
    end
  end
2 Likes

Yeah, that definitely is the solution :partying_face:. I was doing a lot but nothing I was doing solved my problem. Thanks to everybody involved in this discussion. I’ve been trying to solve this for days. My head was about to :exploding_head:

in the validated_sum_100, you were discarding the changesets after applying put_change and validate_number. without trickery, values aren’t mutable in elixir.

you wanted this:

changeset
|> put_change(:sum, sum)
|> validate_number(:sum, equal_to: 100)

or

changeset
changeset_1 = put_change(changeset, :sum, sum)
changeset_2 = validate_number(changeset_1, :sum, equal_to: 100)
1 Like

by the way, I know it’s painful to get used to it now, but 100% you do not want to be dealing, like in python

some_dict = {...}
x = calculate_from(some_dict)

and when stuff stops working you find out that some junior dev put a mutating function into some function called by calculate_from, without telling you, and you didn’t notice because it was on the other side of the codebase.