Syntax to conditional import CSV in Rails - ruby-on-rails

The following import method needs to handle input variables set as nil
def self.import(file, unit_id, profit_center_column [...]
CSV.foreach(file.path, :col_sep => "\t", headers: true, skip_blanks: true) do |row|
begin
Asset.create(
unit_id: unit_id,
profit_center_id: row[profit_center_column] unless profit_center_column.nil?,
[...]
Where the controller launching this method defines
params[:profit_center_column] = #unit.profit_center_column
where #unit.profit_center_column = nil.
I am hitting an unexpected syntax error, unexpected modifier_unless, expecting ')' I have never had to use parenthesis before using unless and I woudl expect the use of the comma here to clearly seperate the statements. Where is the syntax getting messed up?
Rails 4.2.4 and Ruby 2.3.4 are in use.
Note, I've also attempted if statements, but that led to loading random xxx_column information.

You're passing a conditional to the Asset's constructor. If you need to take care of it I'd suggest first initializing Asset with ensured not nil values and afterwards using setters for the remaining attributes:
asset = Asset.new(
unit_id: unit_id,
....
asset.profit_center_id = row[profit_center_column] unless profit_center_column.nil?
asset.save

Related

Refactoring ruby CSV import method

Writing out a rake task to import from csv, one would tend to write
Article.create(
category_id: row[0],
label: row[1],
name: row[2],
integer: row[3],
priority: row[4]
)
But these need to be refactored taking into account a meta-structure where
Class Import
has_many :importattributes
The following is syntactically incorrect
Article.create(
import.importattributes.each do |importattribute|
m_attr = importattribute.mapped_attribute
sequence = importattribute.position.to_i
m_attr: row[sequence]
end
)
on the line complaining about syntax error, unexpected ':', expecting 'end'
m_attr: row[sequence] If that line is commented out, other actions run as they do not parse this error beforehand.
I assume this is mistaken for the proper creation of array of attributes, because an update action, on a per attribute basis
import.importattributes.each do |importattribute|
m_attr = importattribute.mapped_attribute
sequence = importattribute.position.to_i
Article.update(m_attr: row[sequence])
end
does not raise the error.
This is not a valid syntax m_attr: row[sequence]. When used as an argument for update it is treated as a hash { m_attr: row[sequence] } and curly brackets can be omitted.
Article.create(
import.importattributes.each do |importattribute| # returns `import.importattributes`
# and used as argument for `create`
# nothing from `each` block is used
m_attr = importattribute.mapped_attribute # `m_attr` is unused
sequence = importattribute.position.to_i
m_attr: row[sequence] # invalid syntax
# at the very least it has to be wrapped in {} and used with hashrocket syntax
# to make use of m_attr variable.
# { m_attr => row[sequence] }
end
)
create accepts a hash of attributes to create a single record or an array of hashes to create multiple records. The return value of the import logic has to be one of these two options. For clarity, it can be done in three steps:
attributes = {}
import.importattributes.each do |importattribute|
attribute_name = importattribute.mapped_attribute
csv_column = importattribute.position.to_i
attributes[attribute_name] = row[csv_column] }
end
Article.create(attributes)
or in one step using inject
Article.create(
import.importattributes.inject({}) do |attributes, importattribute|
attributes[importattribute.mapped_attribute] = row[importattribute.position.to_i]
attributes
end
)

setting conditional attributes for API connection

A method needs to instantiate a session with various attributes, some of which are optional
session = Checkout::Session.create({
locale: I18n.locale,
reference_id: id,
customer_email: #user_mail,
[...]
})
The last shown attribute, customer_email, is optional but it should not be generated if the value does not exist.
customer_email: #user_mail unless !#user_email,
logically hits a syntax error because an additional param (the comma) is being produced
syntax error, unexpected ',', expecting end
and thus the API expects another attribute.
(customer_email: #user_mail, unless !#user_email)
also fails as there is confusion over the parenthesis
syntax error, unexpected ')', expecting then or ';' or '\n'
How should this syntax be cast?
You need to extract the options hash into a variable and manipulate it before sending it to the Checkout::Session.create.
Something like this:
options = {
locale: I18n.locale,
reference_id: id
}
options[:customer_email] = #user_mail if #user_email
session = Checkout::Session.create(options)

Rails helper pass negative symbol as argument in self method

In my Rails 6, Ruby 2.7 app I'm using ActionView::Helpers::NumberHelper and number_to_currency method. Everything works fine but in one place I need to have negative amount instead of positive. To do so I created two methods:
formatters/document_amount_formatter.rb
module Formatters
# Formats the amount in a suitable form to be used in PDF creator.
class DocumentAmountFormatter
extend ActionView::Helpers::NumberHelper
# The method to call.
#
# #return [String]
def self.call(amount)
number_to_currency(amount.to_f, delimiter: '.', separator: ',', format: '%n €')
end
def self.negative_amount(amount)
number_to_currency(-amount.to_f, delimiter: '.', separator: ',', format: '%n €')
end
end
end
Both works well:
Formatters::CashbookDocumentAmountFormatter.call(cash_transactions.first.gross_amount)
=> "100,00 €"
Formatters::CashbookDocumentAmountFormatter.negative_amount(cash_transactions.first.gross_amount)
=> "-100,00 €"
But I'm not so sure if this is a good approach, tbh the code seems to be smelly. Is it possible to change those two methods into one? How to pass '-' or '+' as an argument inside of one of these methods?
Call call from within negative_amount.
def self.negative_amount(amount)
call(-amount)
end
The next question being, why have this method at all? The caller can write formatter.call(-amount) just as easily and more obviously.
Note that you probably shouldn't hard-code currency formatting and instead make use if internationalization.

Don't change string value on insert

I have a Model user with the following method:
def number_with_hyphen
number&.insert(8, "-")
end
When I run it several times in my tests I get the following output:
users(:default).number_with_hyphen
"340909-1234"
(byebug) users(:default).number_with_hyphen
"340909--1234"
(byebug) users(:default).number_with_hyphen
"340909---1234"
(byebug) users(:default).number_with_hyphen
"340909----1234"
It changes the number ?Here are the docs https://apidock.com/ruby/v1_9_3_392/String/insert
When I restructure my method to:
def number_with_hyphen
"#{number}".insert(8, "-") if number
end
If works like expected. The output stays the same!
How would you structure the code, how would you perform the insert?
which method should I use instead. Thanks
If you're using the insert method, which in the documentation explicitly states "modifies str", then you will need to avoid doing this twice, rendering it idempotent, or use another method that doesn't mangle data.
One way is a simple regular expression to extract the components you're interested in, ignoring any dash already present:
def number_with_hyphen
if (m = number.match(/\A(\d{8})\-?(\d+)\z/))
[ m[1], m[2] ].join('-')
else
number
end
end
That ends up being really safe. If modified to accept an argument, you can test this:
number = '123456781234'
number_with_hyphen(number)
# => "12345678-1234"
number
# => "123456781234"
number_with_hyphen(number_with_hyphen(number))
# => "12345678-1234"
number_with_hyphen('1234')
# => "1234"
Calling it twice doesn't mangle anything, and any non-conforming data is sent through as-is.
Do a clone of the string:
"#{number}".clone.insert(8, '-')

Ruby 2.3.3: Weird Tempfile.new([name, prefix]) basename converted to hash

Testing an upgrade to Ruby 2.3.3 for our Rails 3.2.22.2 application, and getting a weird situation where we are passing an array as the first argument to Tempfile.new, but it's ending up as a hash.
I've patched tempfile.rb to output the basename argument being passed in.
In an irb session (non-Rails), everything is fine:
> require 'tempfile'
true
> Tempfile.new(['test', '.csv'])
["home", ".csv"] # output of basename argument for Tempfile.new
=> #<Tempfile:/var/blah/test###.csv>
In a rails console session:
> Tempfile.new(['test', '.csv'])
{"test"=>nil, ".csv"=>nil}
ArgumentError: unexpected prefix: {"test"=>nil, ".csv"=>nil}
from /path/to/ruby-2.3.3/lib/ruby/2.3.0/tmpdir.rb:113:in `make_tmpname'
Gotta be a gem or something, but can't figure out for the life of me why this is happening or where or what is changing the behavior.
Any ideas or suggestions on how to debug?
In your case I think that somewhere in your code you have the Array#to_hash method defined.
I had the same issue and for some reason when a method has a default param, in this case basename="", and a double splatted parameter, Ruby calls the to_hash function on the first param.
See the following example:
class Dummy
def initialize(val = "", **options)
puts "val = #{val}"
# puts "Options: #{options}"
end
end
class Array
def to_hash
puts "to_hash called on #{self}"
end
end
Dummy.new(["Joe", "Bloe"])
This will output
to_hash called on ["Joe", "Bloe"]
val = ["Joe", "Bloe"]
But when there's no default value for the val param, you'll get:
val = ["Joe", "Bloe"]
Note that the TempFile#initialize function signature was changed from Ruby 2.1 to Ruby 2.2.
Here's the diff:
- def initialize(basename, *rest)
+ def initialize(basename="", tmpdir=nil, mode: 0, **options)
Notice that basename doesn't have a default value anymore.
Just tried this in my console, and got no error. Try a few things,
Make sure you are using ruby 2.3 or higher in your rail app, because I believe that method make_tmpname was handled differently before.
Make sure the quotes around .csv are quotes and not a tilde `.
I get your same error with ruby 2.3.1 if I do this Tempfile.new(['test', /re/])
I hope this helps, at the end of the day what's causing your error is this method try_convert which is returning nil for the second argument you pass to Tempfile.new
This is how I fixed it.
class Tempfile
def initialize(basename="", tmpdir=nil, mode: 0, **options)
warn "Tempfile.new doesn't call the given block." if block_given?
basename = basename.keys if basename.kind_of?(Hash)
#unlinked = false
#mode = mode|File::RDWR|File::CREAT|File::EXCL
::Dir::Tmpname.create(basename, tmpdir, options) do |tmpname, n, opts|
opts[:perm] = 0600
#tmpfile = File.open(tmpname, #mode, opts)
#opts = opts.freeze
end
ObjectSpace.define_finalizer(self, Remover.new(#tmpfile))
super(#tmpfile)
end
end

Resources