Ruby replace if block with guard - ruby-on-rails

I've got a service object which creates the CSV file from assigned data. The call method is pretty simple:
def initialize(data)
#data = data
end
def call
CSV.generate(headers: true, col_sep: ';') do |csv|
csv << csv_headers
data.uniq.each do |contract|
next if contract.transient
payment_details = [
next_payment_date(contract),
I18n.t("contracts.interval_options.#{contract.recurring_transaction_interval&.name}"),
]
csv << payment_details
end
end
end
private
def next_payment_date(contract)
if contract.upcoming_installment.nil?
I18n.t('tables.headers.no_next_payment_date')
else
contract.upcoming_installment.transaction_date.to_s
end
end
It works well but I don't think next_payment_date is really fancy if block, I'm wondering is it possible to replace it with some guard instead?
Because of rubocop I cannot use:
contract.upcoming_installment.nil? ? I18n.t('tables.headers.no_next_payment_date') : contract.upcoming_installment.transaction_date.to_s

In my opinion, the method looks fine as it is. Having better readability is a profitable trade-off for increasing LOCs or introducing methods. That said, there are ways where you can make it an one liner if you really fancy.
contract.upcoming_installment&.transaction_date || I18n.t('tables.headers.no_next_payment_date')
Ruby's safe navigation &. will return nil if upcoming_installment is nil which should fallback to the I18n.

Related

When CSV.generate, generate empty field without ""

Ruby 2.2, Ruby on Rails 4.2
I'm genarating some CSV data in Ruby on Rails, and want empty fields to be empty, like ,, not like ,"", .
I wrote codes like below:
somethings_cotroller.rb
def get_data
respond_to do |format|
format.html
format.csv do
#data = SheetRepository.accounts_data
send_data render_to_string, type: :csv
end
end
end
somethings/get_data.csv.ruby
require 'csv'
csv_str = CSV.generate do |csv|
csv << [1,260,37335,'','','','','','']
...
end
And this generates CSV file like this.
get_data.csv
1,260,37335,"","","","","",""
I want CSV data like below.
1,260,37335,,,,,,
It seems like Ruby adds "" automatically.
How can I do this??
In order to get CSV to output an empty column, you need to tell it that nothing is in the column. An empty string, in ruby, is still something, you'll need to replace those empty strings with nil in order to get the output you want:
csv_str = CSV.generate do |csv|
csv << [1,260,37335,'','','','','',''].map do |col|
col.respond_to?(:empty?) && col.empty? ? nil : col
end
end
# => 1,260,37335,,,,,,
In rails you can clean that up by making use of presence, though this will blank out false as well:
csv_str = CSV.generate do |csv|
csv << [1,260,37335,'',false, nil,'','',''].map(&:presence)
end
# => 1,260,37335,,,,,,
The CSV documentation shows an option that you can use for this case. There are not examples but you can guess what it does.
The only consideration is, you need to send an array of Strings, otherwise, you will get a NoMethodError
csv_str = CSV.generate(write_empty_value: nil) do |csv|
csv << [1,260,37335,'','','','','','', false, ' ', nil].map(&:to_s)
end
=> "1,260,37335,,,,,,,false, ,\n"
The benefit of this solution is, you preserve the false.
I resolved by myself!
in somethings_controller.rb
send_data render_to_string.gsub("\"\"",""), type: :csv

Many very similar functions, spaghetti code fix?

I have approx 11 functions that look like this:
def pending_acceptance(order_fulfillments)
order_fulfillments.each do |order_fulfillment|
next unless order_fulfillment.fulfillment_time_calculator.
pending_acceptance?; collect_fulfillments(
order_fulfillment.status,
order_fulfillment
)
end
end
def pending_start(order_fulfillments)
order_fulfillments.each do |order_fulfillment|
next unless order_fulfillment.fulfillment_time_calculator.
pending_start?; collect_fulfillments(
order_fulfillment.status,
order_fulfillment
)
end
end
The iteration is always the same, but next unless conditions are different. In case you wonder: it's next unless and ; in it because RuboCop was complaining about it. Is there a solution to implement it better? I hate this spaghetti code. Something like passing the condition into "iterate_it" function or so...
edit: Cannot just pass another parameter because the conditions are double sometimes:
def picked_up(order_fulfillments)
order_fulfillments.each do |order_fulfillment|
next unless
order_fulfillment.handed_over_late? && order_fulfillment.
fulfillment_time_calculator.pending_handover?
collect_fulfillments(
order_fulfillment.status,
order_fulfillment
)
end
end
edit2: One question yet: how could I slice a symbol, to get a user role from a status? Something like:
:deliverer_started => :deliverer or 'deliverer'?
You can pass another parameter when you use that parameter to decide what condition to check. Just store all possible conditions as lambdas in a hash:
FULFILLMENT_ACTIONS = {
pending_acceptance: lambda { |fulfillment| fulfillment.fulfillment_time_calculator.pending_acceptance? },
pending_start: lambda { |fulfillment| fulfillment.fulfillment_time_calculator.pending_acceptance? },
picked_up: lambda { |fulfillment| fulfillment.handed_over_late? && fulfillment.fulfillment_time_calculator.pending_handover? }
}
def process_fulfillments(type, order_fulfillments)
condition = FULFILLMENT_ACTIONS.fetch(type)
order_fulfillments.each do |order_fulfillment|
next unless condition.call(order_fulfillment)
collect_fulfillments(order_fulfillment.status, order_fulfillment)
end
end
To be called like:
process_fulfillments(:pending_acceptance, order_fulfillments)
process_fulfillments(:pending_start, order_fulfillments)
process_fulfillments(:picked_up, order_fulfillments)
you can make array of strings
arr = ['acceptance','start', ...]
in next step:
arr.each do |method|
define_method ( 'pending_#{method}'.to_sym ) do |order_fulfillments|
order_fulfillments.each do |order_fulfillment|
next unless order_fulfillment.fulfillment_time_calculator.
send('pending_#{method}?'); collect_fulfillments(
order_fulfillment.status,
order_fulfillment
)
end
end
end
for more information about define_method
While next is handy it comes late(r) in the code and is thus a bit more difficult to grasp. I would first select on the list, then do the action. (Note that this is only possible if your 'check' does not have side effects like in order_fullfillment.send_email_and_return_false_if_fails).
So if tests can be complex I would start the refactoring by expressing the selection criteria and then pulling out the processing of these items (wich also matches more the method names you have given), somewhere in the middle it might look like this:
def pending_acceptance(order_fulfillments)
order_fulfillments.select do |o|
o.fulfillment_time_calculator.pending_acceptance?
end
end
def picked_up(order_fulfillments)
order_fulfillments.select do |order_fulfillment|
order_fulfillment.handed_over_late? && order_fulfillment.
fulfillment_time_calculator.pending_handover?
end
end
def calling_code
# order_fulfillments = OrderFulFillments.get_from_somewhere
# Now, filter
collect_fulfillments(pending_start order_fulfillments)
collect_fulfillments(picked_up order_fulfillments)
end
def collect_fullfillments order_fulfillments
order_fulfillments.each {|of| collect_fullfillment(of) }
end
You'll still have 11 (+1) methods, but imho you express more what you are up to - and your colleagues will grok what happens fast, too. Given your example and question I think you should aim for a simple, expressive solution. If you are more "hardcore", use the more functional lambda approach given in the other solutions. Also, note that these approaches could be combined (by passing an iterator).
You could use something like method_missing.
At the bottom of your class, put something like this:
def order_fulfillment_check(method, order_fulfillment)
case method
when "picked_up" then return order_fulfillment.handed_over_late? && order_fulfillment.fulfillment_time_calculator.pending_handover?
...
... [more case statements] ...
...
else return order_fulfillment.fulfillment_time_calculator.send(method + "?")
end
end
def method_missing(method_name, args*, &block)
args[0].each do |order_fulfillment|
next unless order_fulfillment_check(method_name, order_fulfillment);
collect_fulfillments(
order_fulfillment.status,
order_fulfillment
)
end
end
Depending on your requirements, you could check if the method_name starts with "pending_".
Please note, this code is untested, but it should be somewhere along the line.
Also, as a sidenote, order_fulfillment.fulfillment_time_calculator.some_random_method is actually a violation of the law of demeter. You might want to adress this.

How to refactor nested loops in a Ruby method that exports to CSV?

I have to export some information to CSV. I wrote this code and I don't really like it. I don't know how I can refactor it and get rid of the nested loops.
My relations are the following: Order has many Moves, Move has many Stops.
I have to export all of this to CSV, so I will have multiple lines for the same order.
Here is my (low quality) code:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(self.first.exported_attributes.values.flatten) # headers
self.each do |order|
order.moves.map do |move|
move.stops.map do |stop|
order_data = order.exported_attributes[:order].map do |attributes|
order.public_send(attributes)
end
move_data = order.exported_attributes[:move].map do |attributes|
move.decorate.public_send(attributes)
end
stop_data = order.exported_attributes[:stop].map do |attributes|
stop.decorate.public_send(attributes)
end
csv << order_data + move_data + stop_data
end
end
end
end
end
I did this yesterday:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(self.first.exported_attributes.values.flatten) # headers
self.each do |order|
order.moves.each do |move|
move.stops.each do |stop|
csv << order.exported_attributes[:order].map { |attr| order.public_send(attr) } +
order.exported_attributes[:move].map { |attr| move.decorate.send(attr) } +
order.exported_attributes[:stop].map { |attr| stop.decorate.send(attr) }
end
end
end
end
end
The biggest smell I smell isn't the nested loops, but the near-duplication of how the values are gotten from each model.
Let's extract that duplication into similar methods with the same name, exported_values, on Order, Move and Stop:
class Order
def exported_values
exported_attributes[:order].map { |attrs| { public_send(attrs) }
end
end
class Move
def exported_values
order.exported_attributes[:stop].map { |attrs| { decorate.public_send(attrs) }
end
end
class Stop
def exported_values
move.order.exported_attributes[:move].map { |attrs| { decorate.public_send(attrs) }
end
end
and use them in to_csv:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(first.exported_attributes.values.flatten) # headers
each do |order|
order_values = order.exported_values
order.moves.each do |move|
order_and_move_values = order_values + move.exported_values
move.stops.each do |stop|
csv << order_and_move_values + stop.exported_values
end
end
end
end
end
The above has some additional minor improvements:
Get and concatenate the exported values in the outermost possible loops for efficiency.
Loop over moves and stops with each rather than with map, since the loops are done for side effects rather than return values.
Remove unnecessary uses of self..
Now to_csv isn't so bad. But it still has a little feature envy (that is, it calls too many methods on other objects), so let's extract more methods onto the models:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(first.exported_attributes.values.flatten) # headers
each { |order| order.append_to_csv(csv) }
end
end
class Order
def append_to_csv(csv)
values = exported_values
moves.each { |move| move.append_to_csv(csv, values) }
end
end
class Move
def append_to_csv(csv, prefix)
values = exported_values
stops.each { |stop| stop.append_to_csv(csv, prefix + values) }
end
end
class Stop
def append_to_csv(csv, prefix)
csv << prefix + exported_values
end
end
No more nested loops. The extracted methods are a bit duplicative, but I think if the duplication were extracted they would be unclear.
Next we could try to refactor the exported_values methods into a single method.
Perhaps Order#exported_attributes could be broken up into a method on each class that takes no arguments and returns only that class's exported attributes.
The other difference between the methods is that Order doesn't need .decorator but the other classes do. If it has a decorator, just use that instead of the actual order; if not, just give it a fake one:
class Order
def decorator
self
end
end
You could then define a single exported_values method in an module and include it in all three classes:
def exported_values
exported_attributes.map { |attrs| { decorator.public_send(attrs) }
end
There is one more possible improvement: if it was OK for each model's exported values to remain the same for the lifetime of an instance, you could cache them like this
def exported_values
#exported_values ||= exported_attributes.map { |attrs| { decorator.public_send(attrs) }
end
and inline the values locals in the append_to_csv methods and get the "prefixes" from parent objects in those methods instead of passing them as parameters.
Possibly all of the new methods should be extracted to the decorators rather than to the models; I'm not sure whether your decorators are for CSV generation or only for other purposes.

RSpec testing model method

I have this method in my models/images.rb model. I am starting with testing and having a hard time coming up with tests for it. Would appreciate your help.
def self.tags
t = "db/data.csv"
#arr = []
csvdata = CSV.read(t)
csvdata.shift
csvdata.each do |row|
row.each_with_index do |l, i|
unless l.nil?
#arr << l
end
end
end
#arr
end
First off a word of advice - CSV is probably the worst imaginable data format and is best avoided unless absolutely unavoidable - like if the client insists that manipulating data in MS Excel is a good idea (it is not).
If you have to use CSV don't use a method name like .tags which can confused for a regular ActiveRecord relation.
Testing methods that read from the file system can be quite difficult.
To start with you might want to alter the signature of the method so that you can pass a file path.
def self.tags(file = "db/data.csv")
# ...
end
That way you can pass a fixture file so that you can test it deterministically.
RSpec.describe Image do
describe "tags" do
let(:file) { Rails.root.join('spec', 'support', 'fixtures', 'tags.csv') }
it 'returns an array' do
expect(Image.tags(file)).to eq [ { foo: 'bar' }, { foo: 'baz' } ]
end
end
end
However your method is very ideosyncratic -
def self.tags
t = "db/data.csv"
#arr = []
self.tags makes it a class method yet you are declaring #arr as an instance variable.
Additionally Ruby's enumerable module provides so many methods for manipulating arrays that using an outer variable in a loop is not needed.
def self.tags(file = "db/data.csv")
csv_data = CSV.read(file)
csv_data.shift
csv_data.compact # removes nil elements
end

How to simplify this Rails code -- gets first line from file, parses it and downcases each element

fields = CSV.parse(File.open(filename).first)[0]
fields.each_with_index do |field, i|
fields[i] = field.downcase
end
I want to get the first line from the line, parse it as CSV and make each element lowercase.
This code seems too redundant to me. Any suggestions?
You can make the looping stuff a bit more concise if you wish:
fields.map!(&:downcase)
or even:
fields = CSV.parse(File.open(filename).first)[0].map(&:downcase)
I think you're leaving a file handle hanging there too so you might want to try something like:
fields = []
File.open(filename) do |f|
fields = CSV.parse(f.readline)[0].map(&:downcase)
end
I don't think there's anything wrong with what you have but you could say this:
fields = CSV.parse(File.open(filename, 'r').first).first.map(&:downcase)
Or you could make it easier to read with some methods:
def first_line_of(filename)
File.open(filename, 'r').first
end
def csv_to_array(string)
CSV.parse(string).first
end
def downcase(a)
a.map(&:downcase)
end
fields = downcase csv_to_array first_line_of filename

Resources