Home > front end >  Ruby integer elements in array .map vs .map!? and how to check which code is best?
Ruby integer elements in array .map vs .map!? and how to check which code is best?

Time:11-02

I had a task to output the sum of all command line arguments, and after a bunch of errors and realising ARGV.to_i won't convert all the elements to an integer, I came up with the following solutions.

def argv_sum()
    index = 0
    sum = 0
    ARGV.each do |a|
        ARGV[index] = ARGV[index].to_i
        index  = 1
    end
    puts ARGV.sum
end
argv_sum()

or

def argv_sum()
    index = 0
    sum = 0
    ARGV.each do |a|
        sum  = ARGV[index].to_i
        index  = 1
    end
    puts sum
end
argv_sum()

I then looked online and realised I can convert all elements in array to integer, and I'm assuming (please correct me if I'm wrong), that the below is the best/most efficient code for the program and the only way to convert all elements of an array into an integer on one line and without relying on a loop. I'm also assuming the sum = ARGV[index].to_i is better than ARGV[index] = ARGV[index].to_i

def argv_sum()
    ARGV.map!(&:to_i)
    puts ARGV.sum()
end
argv_sum

But what I'm confused about is map vs map!. I know the former replaces the array with a new one, the latter changes the original array. But if I used map for whatever reason, how would I differentiate between it and the original. For e.g the following (which is nigh identical to the above) doesn't work.

def argv_sum()
    ARGV.map(&:to_i) #map is used here instead of map!
    puts ARGV.sum
end
argv_sum

.map creates a new array, but how do I specifically use this new array or use the old one when I want to? Because the program just assumes that I'm using the old array and so I'd get a "String can't be coerced into integer" error. And it has an identical name to the old array, regardless if I'm using ARGV or something like array_test = []

And with all my methods of solving my original sum problem, is there a way to check which one is most efficient. (best for program, allows program to run more efficiently, faster/ save space/ whatever?) Both for those procedures and in any other program I may want to write in the future.

CodePudding user response:

.map creates a new array, but how do I specifically use this new array or use the old one when I want to?

Ruby, like most programming languages (but not all) has something called variables. A variable provides two things: it allows you to assign a label to a thing, and it allows you to refer back to the thing using the label.

The syntax for assigning a variable is

variable = expression

and from that moment on, the variable variable refers to the object that was returned by evaluating expression. So, you could do something like this:

def argv_sum
  integers = ARGV.map(&:to_i)
  puts integers.sum
end

argv_sum

However, I don't think the variable adds anything to the code, it doesn't really make the code easier to read and/or maintain, so I would just get rid of it:

def argv_sum = puts(ARGV.map(&:to_i).sum)

argv_sum

Remember, the two purposes of a variable are referring back to an object later, and giving it a label. In this case, we are not really referring back to the object later, we are referring back to it immediately, and the label does not really add clarity to the code. So, the variable is just superfluous fluff and can go away.

Note that, as you can see in the documentation, Enumerable#sum takes a block to transform each element before computing the sum … in other words, Enumerable#sum with a block argument fuses the map and the sum together into one operation, so we can actually just do

def argv_sum = puts(ARGV.sum(&:to_i))

argv_sum

Personally, I don't like mixing computation and input/output, because it makes code harder to test. This code, for example, can only be tested by putting it into a script, and then writing a second script which calls the first script with different command line arguments and parses the command line output. That is complex, brittle, and error-prone. It would be much nicer if we could test it by simply calling a method.

So, I would separate the printing and the summing part. Note that the name of the method already suggests that anyway: argv_sum sounds like the method returns the sum of ARGV, but it actually doesn't: it returns nil and only prints the sum of ARGV. So, let's fix that:

def argv_sum = ARGV.sum(&:to_i)

def print_argv_sum = puts(argv_sum)

print_argv_sum

Now, we have separated the input / output part from the computation part … or, have we? No, actually we have not: we have separated the printing part from the computation part, but there is still a somewhat "hidden" input part: ARGV itself is kind-of a "magic" input from the outside world, so we should separate that out, too:

def sum_array_of_strings(ary) = ary.sum(&:to_i)

def print_array_sum(ary, output_stream = default_output) =
  output_stream.puts(sum_array_of_strings(ary))

def print_argv_sum(output_stream = default_output) =
  print_array_sum(argv, output_stream)

def argv = ARGV
def default_output = $>

print_argv_sum

While this is probably overkill for a simple example, this code now allows us to easily test almost all aspects of our code without needing any input / output. We can test whether the summing works by calling sum_array_of_strings and passing it an array of our choosing. We can test whether the printing works by calling print_array_sum and passing it an array of our choosing and a fake output stream (for example an instance of StringIO from the stringio standard library) which we can later inspect. We can test that the whole logic hangs together correctly by overriding the default_output_stream and argv methods to return objects of our choosing.

None of this requires actually passing any command line arguments or parsing the printed output:

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'minitest/autorun'
require 'stringio'

class TestArgvSummer < Minitest::Test
  def setup
    @fake_output = StringIO.new
    @fake_argv = %w[1 2 3 4 5]
  end

  def test_that_sum_array_of_strings_sums_correctly
    assert_equal 0, sum_array_of_strings([])
    assert_equal 42, sum_array_of_strings(%w[42])
    assert_equal 65, sum_array_of_strings(%w[23 42])
    assert_equal 15, sum_array_of_strings(@fake_argv)
  end

  def test_that_print_array_sum_works_correctly
    @fake_output.rewind
    print_array_sum(@fake_argv, @fake_output)
    assert_equal "15\n", @fake_output.string
  end
end

CodePudding user response:

Using Array#sum on ARGV

You don't show your inputs or how you're calling your code, so it's tough to offer advice that fits your specific use case. However, in a more general sense, Ruby has a lot of built-in methods that make summing Integer values pretty straightforward.

ARGV is a special Array that passes arguments into your program as Strings, and you can use Array#sumto add them all up once you convert them to Integers, Floats, or really anything that #responds_to? : . For example, consider the following Ruby script:

#!/usr/bin/env ruby

# call #to_i on each each element of ARGV,
# sum all the elements, and output the
# result
puts ARGV.map(&:to_i).sum

From your shell you can invoke the program like this:

$ ruby sum.rb 5 10 15
30

There's also nothing stopping you from assigning directly to ARGV in irb or pry. Just keep in mind that ARGV always contains String values when coming from the command line, so you have to perform the conversion to a Numeric from a String when you aren't assigning it directly as a non-String inside a REPL.

  • Related