0

Here is a ruby array:

array = ['x', 3, 0, 4, 4, 7]

I want to map through array, take every integer in the array (except for 0 ) minus 2, and return a new array.

When there are letters, no change will be made to the letters.

Example output:

['x', 1, 0, 2, 2, 5]

This is what I have, but I got the error message saying "undefined method integer? "can someone tell me what is the problem?

    def minusNumber(array)
     array.map do |e|
      if e.integer? && e !== 0
      e - 2
      end
     end
    end
Olivia
  • 195
  • 4
  • 14
  • 2
    Use `is_a?` to check the object's class, as `integer?` isn't a method for strings, that's why the NoMethodError. You'd need an else statement, perhaps. And to replace `!==`. – Sebastián Palma Jul 04 '18 at 21:37
  • You have made no specification for non-letter `String`s what should "0" or "2" return? They are not `Integer`s but they are not "letters" either. (for the sake of the question I will assume that punctuation and other non-numeric/non-letter characters should be considered "letters") – engineersmnky Jul 05 '18 at 13:50

6 Answers6

2

Here's another take on this:

array = ['x', 3, 0, 4, 4, 7]

transformed = array.map do |e|
  case e
  when 0, String
    e
  when Integer
    e - 2
  else
    fail 'unexpected input'
  end
end

transformed # => ["x", 1, 0, 2, 2, 5]

It's a pity that you have to keep the elements from which you didn't subtract 2. I really wanted to do something like this

array.grep(Integer).reject(&:zero?).map{|i| i - 2 } # => [1, 2, 2, 5]

Couldn't find a way yet (which preserves the unprocessed items).

Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • 1
    A case statement seems a bit complex for such a simple operation IMO. Not to say this is not a valid solution and works, just thinking a switch is an expensive operation when only really need to check for numbers, and everything else will be ignored. – ForeverZer0 Jul 04 '18 at 22:39
  • @ForeverZer0 It is more expensive how? Performance-wise, it should be identical to an if. And brain-power-wise it should be even cheaper. Reads like engish, no need to decipher any `is_a?` and `== 0`. ¯\\_(ツ)_/¯ – Sergio Tulentsev Jul 05 '18 at 07:41
  • Switch statements inherently have more overhead than `if..end` in pretty much every language, Ruby being no exception. Not to mention equality comparison works differently, `<=>` is also more expensive than basic equality, so it is in multiple ways. – ForeverZer0 Jul 05 '18 at 07:49
  • 1
    @ForeverZer0: no `<=>` here, though – Sergio Tulentsev Jul 05 '18 at 07:49
  • @ForeverZer0: nope, they don't. They use threequals, not the spaceship. Benchmarked mine against yours and yes, yours is 1.5x faster. Still I think this one reads better. – Sergio Tulentsev Jul 05 '18 at 07:52
  • I am incorrect in one regard, `===` is used, not `<=>`, I am too tired, lol, but same theory. – ForeverZer0 Jul 05 '18 at 07:54
  • @ForeverZer0: not only does my code read better, it is also closer to the spec! :) It is literally not specified what to do if it's not a number or string, so I do the most sensible thing. – Sergio Tulentsev Jul 05 '18 at 07:59
2

The other answers here will work fine with your current input. Something like:

def minusNumber(array)
  array.map do |e|
    if e.is_a?(Integer) && e != 0
      e - 2
    else
      e
    end
  end
end

But here is a more flexible solution. This might be a bit too advanced for you where you are now, but all learning is good learning :-)

Ruby is a language that allows polymorphism in its variables. You can see, using the example input for your method, that the variable e may contain a String object or an Integer object. But it actually may contain any type of object, and Ruby would not care one bit, unless it encounters an error in using the variable.

So. In your example, you need to keep Integers in the output. But what if in the future you need to pass in an array containing some Score objects, and you'd need those in your output too? This is a brand new class that you haven't even written yet, but you know you will later on down the line. There's a way you can re-write your method that will anticipate this future class, and all other Integer-type classes you may someday write.

Instead of using #is_a? to check the type of the object, use #respond_to? to check what methods it implements.

Any class that can be used as an integer should implement the #to_int method. Integer certainly does, and your future Score class will, but String does not. And neither does any other class that can't be considered like an integer. So this will work for all types of values, correctly separating those that respond to #to_int and those that don't.

def minusNumber(array)
  array.map do |e|
    if e.respond_to?(:to_int) && e != 0
      e - 2
    else
      e
    end
  end
end

Again, this stuff might be a bit advanced, but it's good to get in the habit early of thinking of variables in terms of its methods as opposed to its type. This way of thinking will really help you out later on.

Nossidge
  • 921
  • 1
  • 12
  • 24
  • Asking for `:to_int` is not enough. `e` would also need to respont to `:-`. For this task, asking to where e responds to, is IMO not appropriate anyway, because the requirement says that the transformation should apply if e *is* an Integer, not if it can be turned into one. Imagine someone one would Monkey-patch `String` to also have a `to_int` method.... What *would* make sense, is to test for `e.class <= Integer` instead of `e.is_a?(Integer)`. – user1934428 Jul 05 '18 at 06:47
  • If it responds to `:to_int` it will respond to `:-`. Implementing `#to_int` is a contract that the writer of the class makes to its users that says, "this can be considered an Integer in ALL cases where an Integer can be used". Inheritance is the best, but not the only, way of abiding by this contract. – Nossidge Jul 05 '18 at 13:16
  • 1
    @user1934428 why would you recommend `<=` over `is_a?`. `is_a?` has a simpler implementation and provides the desired result in this context. e.g. the OP does not need to know if `Integer` is less than `e.class` (`false`) or not in the ancestry chain at all (`nil`) and given the fact that `e` is an instance already `e.is_a?` makes more logical sense since the desired result from `is_a?` and `<=` is an identical call to `class_search_ancestor` without all the other contextual checks that `<=` implements – engineersmnky Jul 05 '18 at 13:21
  • My mistake! I first thought that `is_a?` requires class equality, but of course it works for subclasses too. Hence, `is_a?` is indeed a perfect choice. – user1934428 Jul 06 '18 at 08:25
  • @Nossidge: Can we rely on it? One can create a class which has `:to_int`, and still decide not to implement the arithmetic operations. AFIK, `to_int` is currently used only by subclasses of `Numeric` (for instance, `Float`). If you want to go along the line, that it can be kind of a substitue for Integers, it would make more sense to me to ask for `e.is_a?(Numeric)`, which makes the (not unreasonable) assumption that a class, which behaves like a number, should be derived from `Numeric`. – user1934428 Jul 06 '18 at 08:32
  • @user1934428: I'd consider this to be acceptable. In the future, if a non-arithmetic class were created, the missing `:-` method will create a runtime error. This will alert the tester to this use-case and the code can be altered accordingly. The goal of writing code such as this is to be future-friendly, not future-proof. I'd say `:to_int` gives the most maintenance-bang for your time-buck, and you should deal with weird outliers only if they come up. – Nossidge Jul 06 '18 at 11:31
  • I have to agree with @user1934428 `to_int` has nothing to do with this question IMO. Implementation of `:to_int` does not imply implementation of `:-` and furthermore is now assuming the `:-` is an arithmetical operation based on `to_int`. However if the intended result was to act on things that can be coerced into `Integers` then `to_i` is the correct method to use. (every class that implements `to_int` implements it as `to_i`). `to_i` is explicit and, semantically speaking, implies the return of an `Integer` which guarantees that `- 2` also returns an `Integer`. – engineersmnky Jul 06 '18 at 17:43
  • 1
    @engineersmnky: `:to_i` is absolutely not the message to check a response to; `String` implements `:to_i`, and the use-case specifically states to return strings as-is. See this answer for more about the differences between `:to_i` and `:to_int`: https://stackoverflow.com/a/11182123/139299 – Nossidge Jul 06 '18 at 21:58
  • I understand the difference as noted in my comment “[if the intent] was to act on things that can be coerced in to `Integers`“ Also the post clearly states only `Integer`s are to be acted upon which further supports my statwment – engineersmnky Jul 06 '18 at 22:01
  • 1
    @engineersmnky: We're arguing over a spec that hasn't had its edge-cases sanded off. I think we're both right, depending on what the client wants :-). The purpose behind my answer was to help a newbie start to think of objects not as having a fixed type based on their class, but a flexible type based on their public interface. It's something I wish I'd learnt sooner. – Nossidge Jul 06 '18 at 23:45
0

Short and sweet.

array.map { |v| v.is_a?(Integer) && !v.zero? ? v - 2 : v }

This eliminates all the needless case statements, multiple iteration, and transformations. Simply takes each element, if it is an integer and greater than 0, then subtracts 2, otherwise does nothing.

ForeverZer0
  • 2,379
  • 1
  • 24
  • 32
0

You can do this without checking for type.

use of kind_of? is a code smell that says your code is procedural, not object oriented… https://www.sandimetz.com/blog/2009/06/12/ruby-case-statements-and-kind-of

def someMethod(arr)
  arr.map do |item|
    next item unless item.to_i.nonzero?
    item - 2
  end
end

someMethod(["x", 3, 0, 4, 4, 7])

> ["x", 1, 0, 2, 2, 5]
user3574603
  • 3,364
  • 3
  • 24
  • 59
  • 1
    Honestly I would say this is the best answer excepting that `"2".to_i.nonzero? #=> true` but the fallout is a `NoMethodError` because `String#-` is not a method and even if it was it is unlikely it would accept an `Integer` as its argument – engineersmnky Jul 05 '18 at 13:36
  • `item.inspect.to_i.nonzero?` works in that case. Although, IMHO, I don't think it's ideal. I feel uncomfortable about coercing `item` to a string and back to an integer and I don't think it's clear why or what it's doing anyway. – user3574603 Jul 06 '18 at 15:11
-1

As mentioned in the comment above you should use is_a? method to check the datatype of an element. The code below should work as expected:

 def minusNumber(array)
   array.map do |e|
     if e.is_a?(String) || e == 0
       e
     else
       e - 2
     end
   end
 end

Instead of the if statement you could also do:

(e.is_a?(String) || e == 0 ) ? e : e-2

Please note that the original object is unchanged unless you use map!

John Baker
  • 2,315
  • 13
  • 12
-1

I think this is legible and elegant:

array.map { |v| v.is_a?(Integer) && v == 0 ? v : v -2 }
ricsdeol
  • 159
  • 2
  • 4
  • Lol, almost cut-paste of the solution I had already posted https://stackoverflow.com/questions/51180923/how-to-use-ruby-array-in-this-case – ForeverZer0 Jul 05 '18 at 00:45
  • Not that it generally matters anymore and especially in a ternary situation but a good rule of thumb is that a conditional should check the "most likely" scenarios in order so as to reach the result in the fastest manner. This is especially true with if..elsif conditionals. Thus given the provided input we can divide the results into 2 groups `['x',0]` and `[1,3,4,4,7]` as you can see the second group is larger and thus "more likely" to occur so we should check for this and let group one represent false in this case. However your example checks for group 1. – engineersmnky Jul 05 '18 at 13:30