Validate_number: comparing two fields

Hi, please I have a resource that has two integer fields serial_start and serial_end. I want to be able to validate if one is greater than the other. Below is my code but it doesn’t validate.

defmodule Commercial.Clients.Passbook do
	use Ecto.Schema
	import Ecto.Changeset
	alias Commerical.Clients.Passbook

	@primary_key {:id, :binary_id, autogenerate: true}
	@foreign_key_type :binary_id

	schema "passbooks" do
		field :passbook_number, :integer
		field :serial_end, :integer
		field :serial_start, :integer
		field :active, :boolean, default: true
		belongs_to :account, Commercial.Clients.Account
		belongs_to :customer, Commerical.Clients.Customer


		timestamps()
	end

	@doc false
	def changeset(%Passbook{} = passbook, attrs) do
		passbook
		|> cast(attrs, [:account_id, :customer_id, :passbook_number, :serial_start, :serial_end, :active])
		|> validate_required([:account_id, :customer_id, :passbook_number, :serial_start, :serial_end])
		|> validate_number(:serial_start, less_than: :serial_end)
		|> unique_constraint(:passbook_number, message: "Passbook number already exist.")
	end
end

Please how can i achieve this? Thank you.

I found a way that works but I don’t know if that’s the right approach.

I changed my changeset to below

def changeset(%Passbook{} = passbook, attrs) do
		getSerialStart = Map.get(attrs, "serial_start")
		if getSerialStart do
			{serial_start_numer, _} = Integer.parse(Map.get(attrs, "serial_start"))
		else
			serial_start_numer = 0;
		end

		passbook
		|> cast(attrs, [:account_id, :customer_id, :passbook_number, :serial_start, :serial_end, :active])
		|> validate_required([:account_id, :customer_id, :passbook_number, :serial_start, :serial_end])
		|> validate_number(:serial_end, greater_than: serial_start_numer)
		|> unique_constraint(:passbook_number, message: "Passbook number already exist.")
	end

Any better approach is welcomed.

Untested:

passbook = %{serial_start: serial_start} = passbook
|> cast(…)
|> validate_required(…)

passbook = passbook
|> validate_number(:serial_end, greater_than: serial_start)
|> unique_constraint(…)

Thanks for your response.

I formatted my code as recommended

def changeset(%Passbook{} = passbook, attrs) do
		# getSerialStart = Map.get(attrs, "serial_start")
		# if getSerialStart do
		# 	{serial_start_number, _} = Integer.parse(Map.get(attrs, "serial_start"))
		# else
		# 	serial_start_numer = 0;
		# end
		passbook = %{serial_start: serial_start} = passbook
		|> cast(attrs, [:account_id, :customer_id, :passbook_number, :serial_start, :serial_end, :active])
		|> validate_required([:account_id, :customer_id, :passbook_number, :serial_start, :serial_end])

		passbook = passbook
		|> validate_number(:serial_end, greater_than: serial_start)
		# |> validate_number(:serial_end, greater_than: serial_start_number)
		|> unique_constraint(:passbook_number, message: "Passbook number already exist.")
	end

And i got this error

no match of right hand side value: #Ecto.Changeset<action: nil, changes: %{}, errors: [account_id: {"can't be blank", [validation: :required]}, customer_id: {"can't be blank", [validation: :required]}, passbook_number: {"can't be blank", [validation: :required]}, serial_start: {"can't be blank", [validation: :required]}, serial_end: {"can't be blank", [validation: :required]}], data: #Vokmfi.Clients.Passbook<>, valid?: false>

Oh… Yeah, there is the changeset wrapper… Sorry that I forgot that one…

Then if I recall correctly it has to be this:

passbook = %{data: %{serial_start: serial_start}} = passbook
|> …
1 Like

Thanks so much @NobbZ. It worked.

This is the final code

passbook = %{data: %{serial_start: serial_start}} = passbook
		|> cast(attrs, [:account_id, :customer_id, :passbook_number, :serial_start, :serial_end, :active])
		|> validate_required([:account_id, :customer_id, :passbook_number, :serial_start, :serial_end])

		passbook = passbook
		|> validate_number(:serial_end, greater_than: serial_start)
		|> unique_constraint(:passbook_number, message: "Passbook number already exist.")

@NobbZ, I guess i didn’t test properly. I’ve confirmed that it is not working yet.

Below is the changeset log.

#Ecto.Changeset<action: :insert,
 changes: %{account_id: "f1367ec8-4ad8-4027-9b5f-5df308db764f",
   customer_id: "fbbd8c78-a03d-46e4-a482-efeab078a8f5", passbook_number: 788900,
   serial_end: 450, serial_start: 300},
 errors: [serial_end: {"must be greater than %{number}",
   [validation: :number, number: nil]}], data: #Vokmfi.Clients.Passbook<>,
 valid?: false>

Okay, then this one should ultimatively work (I had missremembered the internal structure and semantics of a change set, I’m not using ecto very often):

passbook = %{data: data, changes: changes} = passbook
|> cast(attrs, [:account_id, :customer_id, :passbook_number, :serial_start, :serial_end, :active])
|> validate_required([:account_id, :customer_id, :passbook_number, :serial_start, :serial_end])

passbook = passbook
|> validate_number(:serial_end, greater_than: changes[:serial_start] || data[:serial_start])
|> unique_constraint(:passbook_number, message: "Passbook number already exist.")

If my internal brain logic isn’t totally failing, this should validate against the new value of serial_start if it is changed, while validating against the old value if not.

I just quickly adjusted this from an ecto validation I do with dates:

@doc """
Validate two integers being one after another

## Examples

    validate_increasing_order(changeset, :from, :to)
    validate_increasing_order(changeset, :from, :to, allow_equal: true)

"""
def validate_increasing_order(changeset, from, to, opts \\ []) do
  {_, from_value} = fetch_field(changeset, from)
  {_, to_value} = fetch_field(changeset, to)
  allow_equal = Keyword.get(opts, :allow_equal, false)

  if compare(from_value, to_value, allow_equal) do
    changeset
  else
    message = message(opts, "must be smaller then #{to}")
    add_error(changeset, from, message, to_field: to)
  end
end

defp compare(f, t, true), do: f <= t
defp compare(f, t, false), do: f < t
9 Likes

Of course a custom validator is probably the best way to go!

@NobbZ
Thanks for your help so far. I got an error:

I want to try the custom validator suggested by @LostKobrakai

Thanks.

@LostKobrakai thanks for you help as well. I have tried the snippet but i got this error

lib/commercial/clients/passbook.ex:53: undefined function message/2 on

message = message(opts, "must be smaller then #{to}")

Please how do i resolve this?

Ah, that’s a helper function I borrowed from ecto’s codebase:

defp message(opts, field \\ :message, message) do
  Keyword.get(opts, field, message)
end

Thanks so much. It worked great! @NobbZ I really appreciate your immense contribution. God bless you in Jesus name, Amen.

Oh yeah, its a struct then, not a plain map, so you need either to implement Access on it, or use dotted acces: changes.serial_start.

1 Like