How to implement an object in Ruby with regex - ruby-on-rails

Absolute beginner in Ruby.
I need to create a class that contains the following keys:
I know that this might be the structure, but can anybody help me with syntax?
class PixKey
def cpf
^[0-9]{11}$
end
def cnpj
^[0-9]{14}$
end
def phone
^\+[1-9][0-9]\d{1,14}$
end
def email
^[a-z0-9.!#$&'*+\/=?^_`{
end
def evp
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
end
end

You can define regular expressions using the /.../ regular expression literal.
Since regular expressions are immutable, I would simply use constants:
class PixKey
CPF = /^[0-9]{11}$/
CNPJ = /^[0-9]{14}$/
PHONE = /^\+[1-9][0-9]\d{1,14}$/
EMAIL = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+#[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
EVP = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
end
In the above, I've changed the email regexp to the one suggested by the HTML standard because the one in your screenshot was probably destroyed by a markdown parser.
You can use the above like this:
PixKey::CPF.match?('12345678901') #=> true
PixKey::CNPJ.match?('12345678901234') #=> true
PixKey::PHONE.match?('+5510998765432') #=> true
PixKey::EMAIL.match?('pix#bcb.gov.br') #=> true
PixKey::EVP.match?('123e4567-e89b-12d3-a456-426655440000') #=> true
Of course, you're not limited to match?, you can use any method from the Regexp class or pattern matching methods from String.
Note that in Ruby, ^ and $ match beginning and end of line which can cause problems in multi-line strings:
string = "before
+5510998765432
after"
string.match?(PixKey::PHONE) #=> true
If you want to match beginning and end of string (i.e. only match whole strings), you can use \A and \z instead:
PixKey::PHONE = /\A\+[1-9][0-9]\d{1,14}\z/
string = "before
+5510998765432
after"
string.match?(PixKey::PHONE) #=> false
string = '+5510998765432'
string.match?(PixKey::PHONE) #=> true

To return the regular expressions for further use, you could return the regex using /:
class PixKey
def cpf
/^[0-9]{11}$/
end
end
You could run PixKey.new.cpf to return the regex:
irb(main):022:0> PixKey.new.cpf
=> /^[0-9]{11}$/
You could also make it a class method by putting self. in front of the method name or add the line class << self as the first line in your class to make them all class methods by default (don't forget the end in this case).
class PixKey
def self.cpf
/^[0-9]{11}$/
end
end
class PixKey
class << self
def cpf
/^[0-9]{11}$/
end
end
end
With this you could run PixKey.cpf to return the regex:
irb(main):022:0> PixKey.cpf
=> /^[0-9]{11}$/

Related

Is there a way to override set intersection in ruby?

i.e. can I define my own '==' method in a class and then make the set intersection operator ('&') use it? Alternatively, is there a way to override the '&' operator itself?
Can we do something like this?
def &(another_object)
#Code for intersection
end
First of all, & is not an operator. It’s a syntactic sugar to call the method & on LHO, passing RHO as the only argument.
Secondary, I doubt whether copy-pasting your attempt to pry/irb and check whether it works is harder/longer than to post a question here.
class MyObject
def ==(other)
other.is_a?(MyObject) && self.&(other).empty?
end
def &(other)
[]
end
end
mo1, mo2 = 2.times.map { MyObject.new }
mo1 == mo2 #⇒ true
mo1 == 42 #⇒ false
can I define my own '==' method in a class
Yes, you can:
class Foo
def ==(other)
# bla
end
end
and then make the set intersection operator ('&') use it?
Yes, you can:
require 'set'
module MySetEqualityExtension
def &(other)
reduce([]) {|acc, el| if acc.any? {|a| a == el } then acc else acc << el end }
end
end
module MySetEquality
refine Set do
prepend MySetEqualityExtension
end
end
Alternatively, is there a way to override the '&' operator itself?
Can we do something like this?
def &(another_object)
#Code for intersection
end
Yes. (As a side-note: it would have taken you less time and effort to just try it instead of asking.)
class Foo
def &(other)
# bla
end
end
You can do all of what you ask, but the correct way would be to simply implement eql? and hash properly, which is what Set#& uses.

Array of objects in ruby

I am trying to learn ruby and have a doubt regarding passing arrays of objects as function parameters and printing it in the function.
I have an array that contains an array of objects as follows
describe Name
par1 = "John"
par2 = "Miley"
par3 = "Maria"
#obj_arr = [Name.new(par1),Name.new(par2),Name.new(par3)]
Name.func1(#obj_arr)
I want to print the name "John", "Miley" and "Maria" in the function and I wrote the function func1 is as follows :
def self.func1(parameter)
parameter.each do |p|
puts p
end
end
This did not print the names. Am I going wrong in accessing the obj_arr in the function?
I think your problem might be the to_s method of the object. You should override it to print what you want. BTW, the syntax in your question is a bit off. I think the definition of the function should be def self.func1 and that your missing an end.
This is the code I tested:
irb(main):001:0> class Name
irb(main):002:1> def self.func1(parameter)
irb(main):003:2> parameter.each do |p|
irb(main):004:3* puts p
irb(main):005:3> end
irb(main):006:2> end
irb(main):007:1> end
=> nil
irb(main):008:0> class Name
irb(main):009:1> def initialize(name)
irb(main):010:2> #name = name
irb(main):011:2> end
irb(main):012:1> end
=> nil
irb(main):013:0> Name.func1([Name.new('a'), Name.new('b')])
#<Name:0x2163dc8>
#<Name:0x2163d98>
=> [#<Name:0x2163dc8 #name="a">, #<Name:0x2163d98 #name="b">]
irb(main):014:0> class Name
irb(main):015:1> def to_s
irb(main):016:2> #name
irb(main):017:2> end
irb(main):018:1> end
=> nil
irb(main):019:0> Name.func1([Name.new('a'), Name.new('b')])
a
b
=> [a, b]
irb(main):020:0>
It may be that func1 is defined on a instance of class Name and not the class itself?
Try:
class Name
def self.func1(parameter)
parameter.each do |p|
puts p
end
end
end

Convert User input to integer

So I have a form where users can input a price. I'm trying to make a before_validation that normalizes the data, clipping the $ if the user puts it.
before_validation do
unless self.price.blank? then self.price= self.price.to_s.gsub(/\D/, '').to_i end
end
If user inputs $50 This code is giving me 0. If user inputs 50$ this code gives me 50. I think since the data type is integer that rails is running .to_i prior to my before_validation and clipping everything after the $. This same code works fine if the data type is a string.
Anyone have a solution that will let me keep the integer datatype?
One way is to override the mechanism on the model that sets the price, like this:
def price=(val)
write_attribute :price, val.to_s.gsub(/\D/, '').to_i
end
So when you do #model.price = whatever, it will go to this method instead of the rails default attribute writer. Then you can convert the number and use write_attribute to do the actual writing (you have to do it this way because the standard price= is now this method!).
I like this method best, but for reference another way to do it is in your controller before assigning it to the model. The parameter comes in as a string, but the model is converting that string to a number, so work with the parameter directly. Something like this (just adapt it to your controller code):
def create
#model = Model.new(params[:model])
#model.price = params[:model][:price].gsub(/\D/, '').to_i
#model.save
end
For either solution, remove that before_validation.
I would define a virtual attribute and do my manipulation there allowing you to format and modify both the getter and setter at will:
class Model < ActiveRecord::Base
def foo_price=(price)
self.price = price... #=> Mods to string here
end
def foo_price
"$#{price}"
end
You also might want to note that:
"$50.00".gsub(/\D/, '').to_i #=> 5000
My soluction
colum price type decimal
t.decimal :price, precision: 12, scale: 6
# app/concern/sanitize_fields.rb
module SanitizeFields
extend ActiveSupport::Concern
def clear_decimal(field)
return (field.to_s.gsub(/[^\d]/, '').to_d / 100.to_d) unless field.blank?
end
def clear_integer(field)
field.to_s.strip.gsub(/[^\d]/, '') unless field.blank?
end
# module ClassMethods
# def filter(filtering_params)
# results = self.where(nil)
# filtering_params.each do |key, value|
# results = results.public_send(key, value) if value.present?
# end
# results
# end
#
# #use
# #def index
# # #products = Product.filter(params.slice(:status, :location, :starts_with))
# #end
#
# end
end
#app/controllers/products_controller.rb
include SanitizeFields
params[:product][:price] = clear_decimal(params[:product][:price])

How do I preserve case with http.get?

I have a requirement to send an HTTP header in a specific character-case. I am aware that this is against the RFC, but I have a requirement.
http.get seems to change the case of the headers dictionary I supply it. How can I preserve the character-case?
Based on the Tin Man's answer that the Net::HTTP library is calling #downcase on your custom header key (and all header keys), here are some additional options that don't monkey-patch the whole of Net::HTTP.
You could try this:
custom_header_key = "X-miXEd-cASe"
def custom_header_key.downcase
self
end
To avoid clearing the method cache, either store the result of the above in a class-level constant:
custom_header_key = "X-miXEd-cASe"
def custom_header_key.downcase
self
end
CUSTOM_HEADER_KEY = custom_header_key
or subclass String to override that particular behavior:
class StringWithIdentityDowncase < String
def downcase
self
end
end
custom_header_key = StringWithIdentityDowncase.new("X-miXEd-cASe")
The accepted answer does not work. Frankly, I doubt that it ever did since it looks like it would have had to also override split and capitalize, I followed that method back a few commits, it's been that way at least since 2004.
Here is my solution, in answer to this closed question:
require 'net/http'
class Net::HTTP::ImmutableHeaderKey
attr_reader :key
def initialize(key)
#key = key
end
def downcase
self
end
def capitalize
self
end
def split(*)
[self]
end
def hash
key.hash
end
def eql?(other)
key.eql? other.key.eql?
end
def to_s
key
end
end
Now you need to be sure to always use instances of this class as your keys.
request = Net::HTTP::Get.new('/')
user_key = Net::HTTP::ImmutableHeaderKey.new("user")
request[user_key] = "James"
require 'stringio'
StringIO.new.tap do |output|
request.exec output, 'ver', 'path'
puts output.string
end
# >> GET path HTTP/ver
# >> Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3
# >> Accept: */*
# >> User-Agent: Ruby
# >> user: James
# >>
Mine is one way to do it, but I recommend doing it as #yfeldblum recommends, simply short-circuit downcase for the header keys that need to have their case left-alone.
In multiple places in Net::HTTP::HTTPHeader the headers get folded to lower-case using downcase.
I think it is pretty drastic to change that behavior, but this will do it. Add this to your source and it will redefine the methods in the HTTPHeader module that had downcase in them.
module HTTPHeader
def initialize_http_header(initheader)
#header = {}
return unless initheader
initheader.each do |key, value|
warn "net/http: warning: duplicated HTTP header: #{key}" if key?(key) and $VERBOSE
#header[key] = [value.strip]
end
end
def [](key)
a = #header[key] or return nil
a.join(', ')
end
def []=(key, val)
unless val
#header.delete key
return val
end
#header[key] = [val]
end
def add_field(key, val)
if #header.key?(key)
#header[key].push val
else
#header[key] = [val]
end
end
def get_fields(key)
return nil unless #header[key]
#header[key].dup
end
def fetch(key, *args, &block) #:yield: +key+
a = #header.fetch(key, *args, &block)
a.kind_of?(Array) ? a.join(', ') : a
end
# Removes a header field.
def delete(key)
#header.delete(key)
end
# true if +key+ header exists.
def key?(key)
#header.key?(key)
end
def tokens(vals)
return [] unless vals
vals.map {|v| v.split(',') }.flatten\
.reject {|str| str.strip.empty? }\
.map {|tok| tok.strip }
end
end
I think this is a brute force way of going about it, but nothing else more elegant jumped to mind.
While this should fix the problem for any Ruby libraries using Net::HTTP, it will probably fail for any gems that use Curl or libcurl.
Joshua Cheek's answer is great, but it does in work anymore in Ruby 2.3
This modification fix it:
class Net::HTTP::ImmutableHeaderKey
...
def to_s
caller.first.match(/capitalize/) ? self : #key
end
end
It all falls down into the net/generic_request#write_header.
You could monkey patch the code
# 'net/generic_request' line 319
def write_header(sock, ver, path)
customheaders = {
"My-Custom-Header" => "MY-CUSTOM-HEADER",
"Another-Custom-Header" => "aNoThErCuStOmHeAdEr"
}
buf = "#{#method} #{path} HTTP/#{ver}\r\n"
each_capitalized do |k,v|
customheaders.key?(k) ? kk = customheaders[k] : kk = k
buf << "#{kk}: #{v}\r\n"
end
buf << "\r\n"
sock.write buf
end
and you don't need to rewrite the whole net/http/header, net/generic_request and net/http chain.
It's not the best solution, but it's the easiest one I guess and there's least amount of monkey patching.
Hope it helps.

Decimals and commas when entering a number into a Ruby on Rails form

What's the best Ruby/Rails way to allow users to use decimals or commas when entering a number into a form? In other words, I would like the user be able to enter 2,000.99 and not get 2.00 in my database.
Is there a best practice for this?
Does gsub work with floats or bigintegers? Or does rails automatically cut the number off at the , when entering floats or ints into a form? I tried using self.price.gsub(",", "") but get "undefined method `gsub' for 8:Fixnum" where 8 is whatever number I entered in the form.
I had a similar problem trying to use localized content inside forms. Localizing output is relatively simple using ActionView::Helpers::NumberHelper built-in methods, but parsing localized input it is not supported by ActiveRecord.
This is my solution, please, tell me if I'm doing anything wrong. It seems to me too simple to be the right solution. Thanks! :)
First of all, let's add a method to String.
class String
def to_delocalized_decimal
delimiter = I18n::t('number.format.delimiter')
separator = I18n::t('number.format.separator')
self.gsub(/[#{delimiter}#{separator}]/, delimiter => '', separator => '.')
end
end
Then let's add a class method to ActiveRecord::Base
class ActiveRecord::Base
def self.attr_localized(*fields)
fields.each do |field|
define_method("#{field}=") do |value|
self[field] = value.is_a?(String) ? value.to_delocalized_decimal : value
end
end
end
end
Finally, let's declare what fields should have an input localized.
class Article < ActiveRecord::Base
attr_localized :price
end
Now, in your form you can enter "1.936,27" and ActiveRecord will not raise errors on invalid number, because it becomes 1936.27.
Here's some code I copied from Greg Brown (author of Ruby Best Practices) a few years back. In your model, you identify which items are "humanized".
class LineItem < ActiveRecord::Base
humanized_integer_accessor :quantity
humanized_money_accessor :price
end
In your view templates, you need to reference the humanized fields:
= form_for #line_item do |f|
Price:
= f.text_field :price_humanized
This is driven by the following:
class ActiveRecord::Base
def self.humanized_integer_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? val.to_i.with_commas : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(","))
end
end
end
def self.humanized_float_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? val.to_f.with_commas : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(","))
end
end
end
def self.humanized_money_accessor(*fields)
fields.each do |f|
define_method("#{f}_humanized") do
val = read_attribute(f)
val ? ("$" + val.to_f.with_commas) : nil
end
define_method("#{f}_humanized=") do |e|
write_attribute(f,e.to_s.delete(",$"))
end
end
end
end
You can try stripping out the commas before_validation or before_save
Oops, you want to do that on the text field before it gets converted. You can use a virtual attribute:
def price=(price)
price = price.gsub(",", "")
self[:price] = price # or perhaps price.to_f
end
Take a look at the i18n_alchemy gem for date & number parsing and localization.
I18nAlchemy aims to handle date, time and number parsing, based on current I18n locale format. The main idea is to have ORMs, such as ActiveRecord for now, to automatically accept dates/numbers given in the current locale format, and return these values localized as well.
I have written following code in my project. This solved all of my problems.
config/initializers/decimal_with_comma.rb
# frozen_string_literal: true
module ActiveRecord
module Type
class Decimal
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
class Float
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
class Integer
private
alias_method :cast_value_without_comma_separator, :cast_value
def cast_value(value)
value = value.gsub(',', '') if value.is_a?(::String)
cast_value_without_comma_separator(value)
end
end
end
end
module ActiveModel
module Validations
class NumericalityValidator
protected
def parse_raw_value_as_a_number(raw_value)
raw_value = raw_value.gsub(',', '') if raw_value.is_a?(::String)
Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
end
end
end
end
I was unable to implement the earlier def price=(price) virtual attribute suggestion because the method seems to call itself recursively.
I ended up removing the comma from the attributes hash, since as you suspect ActiveRecord seems to truncate input with commas that gets slotted into DECIMAL fields.
In my model:
before_validation :remove_comma
def remove_comma
#attributes["current_balance"].gsub!(',', '') # current_balance here corresponds to the text field input in the form view
logger.debug "WAS COMMA REMOVED? ==> #{self.current_balance}"
end
Here's something simple that makes sure that number input is read correctly. The output will still be with a point instead of a comma. That's not beautiful, but at least not critical in some cases.
It requires one method call in the controller where you want to enable the comma delimiter. Maybe not perfect in terms of MVC but pretty simple, e.g.:
class ProductsController < ApplicationController
def create
# correct the comma separation:
allow_comma(params[:product][:gross_price])
#product = Product.new(params[:product])
if #product.save
redirect_to #product, :notice => 'Product was successfully created.'
else
render :action => "new"
end
end
end
The idea is to modify the parameter string, e.g.:
class ApplicationController < ActionController::Base
def allow_comma(number_string)
number_string.sub!(".", "").sub!(",", ".")
end
end
You can try this:
def price=(val)
val = val.gsub(',', '')
super
end

Resources