Help with simple calculator exercise (ruby)

# Custom error class for unsupported operations
class UnsupportedOperation < StandardError
  def initialize(msg = "This operation is not supported.")
    super(msg)
  end
end

class SimpleCalculator
  # Define the allowed operations
  ALLOWED_OPERATIONS = ['+', '*', '/'].freeze

  def self.calculate(first_operand, second_operand, operation)
    # Validate input types for operands
    unless first_operand.is_a?(Integer) && second_operand.is_a?(Integer)
      raise ArgumentError.new("Invalid argument: operands must be integers.")
    end

    # Check if the operation is supported, otherwise raise an UnsupportedOperation error
    unless ALLOWED_OPERATIONS.include?(operation)
      raise UnsupportedOperation.new("Operation '#{operation}' is not supported.")
    end

    # Perform the operation
    case operation
    when "+"
      "#{first_operand} + #{second_operand} = #{first_operand + second_operand}"
    when "*"
      "#{first_operand} * #{second_operand} = #{first_operand * second_operand}"
    when "/"
      # Handle division by zero
      if second_operand == 0
        return "Division by zero is not allowed."
      else
        "#{first_operand} / #{second_operand} = #{first_operand / second_operand}"
      end
    end
  end
end

This is the error I get

NameError: uninitialized constant SimpleCalculator::UnsupportedOperation

Traceback (most recent call first):
    Line 30:in `test_raises_exception_for_non_valid_operations'

Can someone explain why?

I edited your post for you to add codeblocks so it’s easier to read :)

1 Like

Did you initialize a SimpleCalculator::UnsupportedOperation value?

1 Like

As @IsaacG alludes, The tests expect a SimpleCalculator::UnsupportedOperation.

In your code I see an UnsupportedOperation class being created. but I don’t see a SimpleCalculator::UnsupportedOperation class being created :slight_smile:

1 Like

Oh wow I just needed to put the Unsupported < StandardError class in the Simple Calculator class. I was so confused why it wasnt working. Thank you for pointing me in the right direction!

2 Likes

I was stuck at the same point; I “cheated” by looking up community solutions and drew the (apparently mistaken) conclusion that Ruby expects exceptions used by a class method to be defined inside that class. Then I moved on to the Moviegoer exercise and was confused, because the exception definition in that one was expected to be outside the other class definition, and having it inside resulted in an error. Fortunately, ChatGPT helped me realise that the test had a different expectation in each case, and otherwise both implementations were correct in general.

I believe the exercise should explicitly state which scope the test expects us to use in each case, and clarify that this is not a general principle (by saying something like “For this exercise, …”) to avoid misconceptions like the one I originally had. (Alternatively, the test could check which of the two definitions exists and look for when that exception is raised; then both implementations would pass.)

Whilst a hint may help some, the “expected” flow is:

  • you check the tests
  • you see SimpleCalculator::UnsupportedOperation
  • you implement UnsupportedOperation as inner class of SimpleCalculator

Even if you don’t look at the tests, it’s:

  • you implement to the best of your ability
  • you run the tests
  • the failure message alludes to an inner class
  • you fix the implementation

With the learning mode in place, by the time you make this exercise, you should know about A::B syntax and inner classes. A hint will help for this exercise, but the underlying problem of someone not recognising the error message syntax will lead to more problems in more exercises.

Thanks for the reply. You do have a point about checking the error message, but to think of nesting a class inside another class is quite the conceptual leap. I think this possibility should have been mentioned in one of the earlier exercises.

In Moviegoer the relevant error message refers to MoviegoerTester::NotMovieClubMemberError, where it may also be a bit of a leap to deduce that NotMovieClubMemberError must be global scope. (But then again, the initial code given has it at global scope, so in retrospect there was a big hint right there.)

Of course I don’t claim to speak for everyone, this was just my own impression. Perhaps it’s a question of skill, and these things were obvious to other people.

It does seem, however, that if someone completes Simple Calculator but not Moviegoer, they may come to the same mistaken conclusion as I did (based on the fact that the tests reject one of two legitimate solutions). That’s why I’m suggesting some sort of clarification about that.

The tests are not meant to be exhaustive, either, but if the tests reject a legitimate solution, we should be aware of that. Can you let us know what that “legitimate solution” is, so we can evaluate if it is indeed legitimate (the tests dictate, usually, what “legitimate” means, since, while they are not meant to be exhaustive, they should not disallow a valid solution) This is a concept exercise, and so it makes sense that some “otherwise valid solutions” would potentially be invalidated by the tests if the concept is not being covered.

Without seeing your “otherwise valid” solution and what the result of that was, it is hard to evaluate your statement though.

Also, requesting review/mentoring may also be helpful, you stated recently that you have completed the track, I am presuming this means all the concept exercises and the learning exercises! (Congratulations, by the way, that is not a simple task to do!)

Requesting a review/mentoring means that the discussion can happen there with code “in hand”.

You are absolutely right about the communication being that the exception is coming from a “*Tester” constant. When I first saw something like this it was a but of a “what?” moment as well.

But also, the big hint you mentioned about the initial code being right there is a pretty big linkage, if one is paying attention (It is not always easy to pick up on that, though, until you see the link.)

Sure! Here you go :slight_smile:

class UnsupportedOperation < StandardError
end

class SimpleCalculator
  ALLOWED_OPERATIONS = ['+', '/', '*'].freeze

  def self.calculate(first_operand, second_operand, operation)
    unless ALLOWED_OPERATIONS.include?(operation)
      raise UnsupportedOperation.new('This operation is not supported.')
    end
    unless ((Integer===first_operand or Float===first_operand) and (Integer===second_operand or Float===second_operand))
      raise ArgumentError.new('These are not numbers, silly!')
    end
    case operation
    when "+" then result = first_operand + second_operand
    when "*" then result = first_operand * second_operand
    when "/" then
      begin
        result = first_operand / second_operand
      rescue ZeroDivisionError => divide_by_zero
        return "Division by zero is not allowed."
      end
    end
    "#{first_operand.to_s} #{operation} #{second_operand.to_s} = #{result}"
  end
end

Thank you for taking an interest, by the way.

This portion, or where you went with the code, is likely better served with a “Review” or “Mentor” session. There is a bit here to cover for a learner.

I would be happy to go over this there, if you would like.

That said, and while there is a lot that we can talk about in that code, the bottom line is that it is not a valid solution, since the exception class is not in the correct scope, hence the test failure. So “valid solution” if it were not for the tests having the definitive specification of what it requires.

If the :: (scope operator) is not directly mentioned either at this point, or at an earlier point, then perhaps we should make a note of that explicitly. You have been through the concept exercises more recently than I have been, is there a place you feel it would make sense to have it added to the curriculum?

But why does the test not accept both scopes? According to the problem statement, a global-scope exception should be valid. The scope requirement is never explicitly stated; the only way to find out about it is to try the solution that the test rejects. And then it makes sense to think “Oh, so global scope is not accepted, but why wouldn’t it? That must mean that global-scope exceptions are forbidden in Ruby!” (which is not true).

About your suggestion: while it’s true that :: is never explicitly defined, it’s used so many times in the curriculum that its meaning can be inferred from context (though I suppose it doesn’t hurt to add the definition to one of the first exercises). Speaking for myself, what confused me is that exceptions are classes, and despite the message I got implying that the exception is inside SimpleCalculator, I didn’t think it was possible for a class to be inside another class. What would have helped me is if I had encountered a nested class before. Maybe add one such example somewhere along the way?

The problem specification may come from Problem Specifications which is global. However, this is often not necessarily true for concept exercises.

I just read the README.md file and the HINTS.md file and I did not recognize anything that states this. That said, the tests are the specification, the other things are “BOSS SPEAK” and accurately, but not precisely, describes what is necessary, the tests are the measure and information for the details. This also emulates “test drive development” where the tests help to drive a solution. Fortunately, and with some continuous care, the Ruby track works hard to enforce a concept is used, but not necessarily how it is solved within those limits. In this case, it gives room in being able to solve this in various ways, to explore scope changes, while allowing us to check the specific scope is finally adhered to. (We will very likely see this during the mentoring/review process in action.)

Well, I would say that scope operator is bound to be introduced and made to be looked at more closely in some place, and as this is the first place that you have noticed where it differs, it could be that this is exactly the right exercise to mention it.

In learning (concept exercises) it is OK to show but not tell until something changes, and then to challenge what was accepted as “working” before, but to show details when we are ready to highlight when or how it can be different. This might be where we show that there is more to the story than what has been so far observed and accepted.

This isn’t how it works, though. Unless it is stated that a specific way of solving the problem is invalid, then it follows that that solution has to be valid. Since the instructions don’t state that global-scope exceptions are disallowed, then either the instructions are inaccurate or solutions using global-scope exceptions must be accepted.

A corollary of your statement is that a test can never be wrong, no matter what; it is by definition impossible to reject a correct solution. So one could write literally anything in there and there would be no issue.

I could accept this, if not for two problems:

  • It is never communicated to the student. Your post is the first time I hear that the given instructions are allowed to be inaccurate, and that the “real requirements” are whatever the tests are testing for.
  • Since we have no access to the test code, the only way for us to learn the “real requirements” is to get them wrong. This is bad.

I mean, if we’re going to accept hidden requirements, then the subjective experience of trying and unexpectedly failing and developing some why-can’t-they-just-accept-something-that-works frustration would be all-too-common.


My post was originally going to stop here. But fortunately, before posting this, I randomly decided to try my hand at a C-track exercise that was labelled “hard”. Now I think I finally see why we don’t understand each other.

The exercise I looked at was completely different to what I had seen on the Ruby track (maybe because those were learning exercises?) which is to say:

  • The instructions were not specific. They didn’t state the exact form of the output expected. This made it very clear that I would have to look for details somewhere else.
  • The test code was right there on the next tab.

So… I think you may have have had this format in mind all along. But in the Ruby learning track, the test code is not available (unless someone thinks to check github, I guess?) and instructions were always crystal clear, everything spelled out, nothing left unsaid. There was no indication that the tests could enforce further requirements than what we had been told. So when a test unexpectedly rejects something, the reaction isn’t “oh ok, they want this not that” but “why does the test reject solutions using X? so X must be wrong in ruby!” (and bear in mind that this only happens in SimpleCalculator and Moviegoer, the last two exercises of the learning track).

I haven’t tried many tracks yet, but I’m guessing this is a learning track vs practice track difference.

I did not realise this is a learning / concept exercise.

For those, the instructions must be leading, whilst, effectively, tests are still the specification. You should not need the tests for concept exercises.

That also means there is no shared problem specification like Victor mentions.

That’s exactly it.

@kotp has been around before this even existed btw.


I think we should consider a change to make the experience better here as this is a learning exercise. If that’s docs, that’s fine. If that’s code… that’s fine too.

1 Like

And I agree, as stated earlier. “If the :: (scope operator) is not directly mentioned either at this point, or at an earlier point, then perhaps we should make a note of that explicitly.”

2 Likes

Issue: Two learning exercises on the Ruby track have unnecessarily restrictive tests · Issue #6772 · exercism/exercism · GitHub
PR: Moviegoer and SimpleCalculator accept exceptions from both scopes by VaiaPatta1985 · Pull Request #1816 · exercism/ruby · GitHub

2 Likes

Thank you for making the PR. After we are finished with the review, we will see that there are a few scope considerations that we are working with, one at main scope, the other inside the module or class, and there is a third situation as well.

Having gone through the mentor/review process, it may modify your opinion as to how to address this, compared to the current solution. Also, by removing the contention, the potential to lose a learning point can happen as well and may become unnoticed as well.

All of that to say that now that it is in the queue, I will reopen the exercism/ruby#1816 but place it in “draft” mode for now.

3 Likes