Dimitri de Ruiter Dimitri de Ruiter - 26 days ago 18
Ruby Question

Rails 5 - iterate until field matches regex

In my app that I am building to learn Rails and Ruby, I have below iteration/loop which is not functioning as it should.

What am I trying to achieve?



I am trying to find the business partner (within only the active once (uses a scope)) where the value of the field
business_partner.bank_account
is contained in the field
self_extracted_data
and then set the business partner found as self.sender (self here is a
Document
).

So once a match is found, I want to end the loop. A case exists where no match is found and sender = nil so a user needs to set it manually.

What happens now, is that on which ever record of the object I save (it is called as a callback before_save), it uses the last identified business partner as sender and the method does not execute again.

Current code:



def set_sender
BusinessPartner.active.where.not(id: self.receiver_id).each do |business_partner|
bp_bank_account = business_partner.bank_account.gsub(/\s+/, '')
rgx = /(?<!\w)(#{Regexp.escape(bp_bank_account)})?(?!\‌​w)/
if self.extracted_data.gsub(/\s+/, '') =~ rgx
self.sender = business_partner
else
self.sender = nil
end
end
end


Thanks for helping me understand how to do this kind of case.
p.s. have the pickaxe book here yet this is so much that some help / guidance would be great. The regex works.




Using feedback from @moveson, this code works:

def match_with_extracted_data?(rgx_to_match)
extracted_data.gsub(/\s+/, '') =~ rgx_to_match
end

def set_sender
self.sender_id = matching_business_partner.try(:id) #unless self.sender.id.present? # Returns nil if no matching_business_partner exists
end

def matching_business_partner
BusinessPartner.active.excluding_receiver(receiver_id).find { |business_partner| sender_matches?(business_partner) }
end

def sender_matches?(business_partner)
rgx_registrations = /(#{Regexp.escape(business_partner.bank_account.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.registration.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.vat_id.gsub(/\s+/, ''))})/
match_with_extracted_data?(rgx_registrations)
end

Answer

In Ruby you generally want to avoid loops and #each and long, procedural methods in favor of Enumerable iterators like #map, #find, and #select, and short, descriptive methods that each do a single job. Without knowing more about your project I can't be sure exactly what will work, but I think you want something like this:

# /models/document.rb

class Document < ActiveRecord::Base

  def set_sender
    self.sender = matching_business_partner.try(:id) # Returns nil if no matching_business_partner exists
  end

  def matching_business_partners
    other_business_partners.select { |business_partner| account_matches?(business_partner) }
  end

  def matching_business_partner
    matching_business_partners.first
  end

  def other_business_partners
    BusinessPartner.excluding_receiver_id(receiver_id)
  end

  def account_matches?(business_partner)
    rgx = /(?<!\w)(#{Regexp.escape(business_partner.stripped_bank_account)})?(?!\‌​w)/
    data_matches_bank_account?(rgx)
  end

  def data_matches_bank_account?(rgx)
    extracted_data.gsub(/\s+/, '') =~ rgx
  end

end

# /models/business_partner.rb

class BusinessPartner < ActiveRecord::Base

  scope :excluding_receiver_id, -> (receiver_id) { where.not(id: receiver_id) }

  def stripped_bank_account
    bank_account.gsub(/\s+/, '')
  end

end

Note that I am assigning an integer id, rather than an ActiveRecord object, to self.sender. I think that's what you want.

I didn't try to mess with the database relations here, but it does seem like Document could include a belongs_to :business_partner, which would give you the benefit of Rails methods to help you find one from the other.

EDIT: Added Document#matching_business_partners method and changed Document#set_sender method to return nil if no matching_business_partner exists.