Banking on Ruby - Accepts a deliberate error


#1

The code I provided in the online editor was accepted but shouldn’t (as I understand it).
I’m a beginner to Ruby, so I stand corrected. Unless I’m missing something, this should throw an error.

When defining the methods ‘display_balance’ and ‘withdraw’, I used an instance variable ‘@pin’ instead of ‘pin’ in the ‘if statement’ and it was accepted by editor!

The code:

  # Public method (Display balance)
  public
  def display_balance(pin_number)
    # Check PIN number
    if pin_number == @pin # <– This is wrong, but it's accepted by the online editor! 
      puts "Balance: $#{@balance}."
      else
      puts pin_error
    end
  end

  # Public method (Make a withdrawal)
  public
  def withdraw(pin_number, amount)
    # Check PIN number
    if pin_number == @pin  # <–This is wrong, but it's accepted by the online editor! 
      @balance -= amount
    	puts "Withdrew #{amount}. New balance: $#{@balance}."
    else
      puts pin_error
    end
  end
end

Can anyone explain this to me please?


#2

@pin is not a private variable so is legitimate. It does not throw an error. The method is just a little more descriptive.

Well Done!


#3

Thanks for the reply ‘mtf’.

So how does one call the following method (with the correct pin) …

checking_account.display_balance(1234)

…without getting this error?

Access denied: incorrect PIN.

#4

Not sure what the cause is. Please post your complete code so we can test it.


#5

Sure, here you go…

# Super class
class Account
  # Readable attributes
  attr_reader :name
  attr_reader :balance

  # Boot the object
  def initialize(name, balance=100)
    @name = name
    @balance = balance
  end

  # Private method (PIN number)
  private
  def pin
    @pin = 1234
  end

  # Private method (PIN error)
  private
  def pin_error
    return "Access denied: incorrect PIN."
  end

  # Public method (Display balance)
  public
  def display_balance(pin_number)
    # Check PIN number
    if pin_number == @pin
      puts "Balance: $#{@balance}."
      else
      puts pin_error
    end
  end

  # Public method (Make a withdrawal)
  public
  def withdraw(pin_number, amount)
    # Check PIN number
    if pin_number == @pin
      @balance -= amount
    	puts "Withdrew #{amount}. New balance: $#{@balance}."
    else
      puts pin_error
    end
  end
end

# Open an account
checking_account = Account.new("Clive", 100_000)

# Test:
puts checking_account.display_balance(1234) # => 'Access denied: incorrect PIN.'
puts checking_account.withdraw(1234, 500) # => 'Access denied: incorrect PIN.'

#6

Apart from slightly reorganizing your code, the only real change is pin vs. @pin.

class Account
  attr_reader :name, :balance

  def initialize(name, balance=100)
    @name = name
    @balance = balance
  end

  public
  def display_balance(pin_number)
    if pin_number == pin
      puts "Balance: $#{@balance}."
      else
      puts pin_error
    end
  end

  def withdraw(pin_number, amount)
    if pin_number == pin
      @balance -= amount
    	puts "Withdrew #{amount}. New balance: $#{@balance}."
    else
      puts pin_error
    end
  end

  private
  def pin
    @pin = 1234
  end
  def pin_error
    return "Access denied: incorrect PIN."
  end
end

checking_account = Account.new("Clive", 100_000)

checking_account.display_balance(1234)
checking_account.withdraw(1234, 500)
Balance: $100000.
Withdrew 500. New balance: $99500.

What’s weird in my code is that @pin is readable, even though it is private and defined in the pin method. Have no explanation for that.

banking_on_ruby
class Account
  attr_reader :name, :balance
  def initialize(name, balance=100)
    @name = name
    @balance = balance
  end
  public
  def display_balance(pin_number)
    puts pin_number == pin ?
      "Balance: $#{balance}." : pin_error
  end
  def withdraw(pin_number, amount)
    if pin_number == @pin    # with @
      if @balance >= amount
        @balance -= amount
        puts "Withdrew #{amount}. New balance: $#{@balance}."
      else
        puts "Insufficient funds to cover withdrawal."
      end
    else
      puts pin_error
    end
  end
  def deposit(pin_number, amount)
    if pin_number == pin     # without @
      @balance += amount
      puts "Deposit #{amount}. New balance: $#{@balance}."
    else
      puts pin_error
    end
  end
  private
  def pin
    @pin = 1234
  end
  def pin_error
    "Access denied: incorrect PIN."
  end
end
checking_account = Account.new("Wee Gillis", 1_000_000)
checking_account.display_balance(1234)
checking_account.withdraw(1234,1_000)
checking_account.withdraw(1234,1_000_000)
checking_account.deposit(1234, 100_000)
Balance: $1000000.
Withdrew 1000. New balance: $999000.
Insufficient funds to cover withdrawal.
Deposit 100000. New balance: $1099000.

#7

It’s weird isn’t it ‘mtf’?
Your solution basically confirms what I said in the first example of code I posted where I said ‘@pin’ is wrong, but accepted by the online editor in the course.

To be honest, I had a duplicate copy of the file with the same solution for ‘@pin’ but was not sure if it was the correct solution, though it worked. As a beginner to Ruby (or any other form of programming), I’m never sure if a solution works, to accept it at face value. I never know if it’ll give me a headache further down the road should the app grows in complexity. You know what I mean don’t you?

Never-the-less… I truly appreciate your input and would appreciate it if you came back to me should you manage to get an explanation to the thing that puzzles you when you said:

Many thanks! :wink:


#8

Hopefully someone with more Ruby wherewithal will pipe in and clear it up for both of us. I’ve obviously not helped.


#9

No, on the contrary, you have helped me. Your code differs from mine and I’ve adapted some of my code to reflect your approach to the same problem. Nothing was in vain.

:ok_hand:


#10

Found some reading…

How to make instance variables private in Ruby?

Interesting read, but I’m still wondering how your code tripped the wire and mine didn’t. Either something is right in yours, and wrong in mine, or vice versa. Hope to get somewhere further on this in the coming days.


#11

Ok, I think (not 100% sure) I have figured this out after spending hours playing around with this code.

What I have noticed in the original code I provided is that when an instance of the ‘Account’ class is created, only 2 values persist when the object initializes: ‘@name’ and ‘@balance’. So, there is no way to read the value of ‘@pin’ inside the private method after the object initializes.

So, I have come up with a few solutions that work.

Option 1:
Initialize with the private method ‘pin’ containing our ‘@pin’ instance variable:

  # Initialize the object
  def initialize(name, balance=100)
    @name = name
    @balance = balance
    pin # Call private method 'pin' on initialization – reads the value of '@pin'
  end

The private method ‘pin’ can stay as it is because we can read the value of ‘@pin’ during initialization. The value will persist on all instances created with this class – not very useful and we loose flexibility!

  # Private method (PIN number)
  private
  def pin
    @pin = 1234
  end

Create a new instance of the class like this:

checking_account = Account.new("Bruce", 100_000)

Option 2:
Initialize with all the necessary arguments and instance variables

  # Initialize the object with all required arguments
  def initialize(name, pin, balance=100)
    @name = name
    @balance = balance
    @pin = pin
  end

Now the private method ‘pin’ needs adjusting and we have to remove the hard-coded value of ‘@pin’. We will now pass it’s value via the ‘pin’ parameter when we create a new instances of the class.

  # Private method (PIN number)
  private
  def pin
    # @pin = 1234
    @pin # This value will now be assigned via the parameter
  end

Now we can create a new instance of the class like this with the pin ‘1234’:

checking_account = Account.new("Bruce", 1234, 100_000)

I have found that with both techniques, the value of ‘@pin’ is protected and can’t be accessed or changed once an instance of the Class is created. This is the behavior we want from what I can understand.

Well, this is my understanding of it at this stage. Whether it’s correct, is another matter.

:wink:

I have included all my code should anyone like to review it:
Uncomment lines one-at-a-time to see output

# Super class
class Account
  # Readable attributes
  attr_reader :name, :balance

  # Boot the object and set values
  def initialize(name, pin, balance=100)
    # Instance variables: Only accessible from within instance of the class
    @name = name
    @balance = balance
    @pin = pin
  end

  # Private method (PIN number)
  private
  def pin
    @pin
  end

  # Private method (PIN error)
  private
  def pin_error
    return "Access denied: incorrect PIN."
  end

  # Public method (Display balance)
  public
  def display_balance(pin)
    # Check PIN number
    if pin == @pin
      puts "Balance: $#{@balance}."
      else
      puts pin_error
    end
  end

  # Public method (Make a withdrawal)
  public
  def withdraw(pin, amount)
    # Check PIN number
    if pin == @pin
      @balance -= amount
    	puts "Withdrew #{amount}. New balance: $#{@balance}."
    else
      puts pin_error
    end
  end
end

# Open an account (instance of the Account Class)
#checking_account = Account.new # => wrong number of arguments (0 for 2..3) (ArgumentError)
#checking_account = Account.new("Bruce") # => wrong number of arguments (1 for 2..3) (ArgumentError)
#checking_account = Account.new("Bruce", 1234) # => Works (with default balance)
checking_account = Account.new("Bruce", 1234, 1_000_000) # => Works (with custom balance)


# Test: Display Balance
#puts checking_account.display_balance # => wrong number of arguments (0 for 1) (ArgumentError)
#puts checking_account.display_balance(12345) # => Access denied: incorrect PIN.
puts checking_account.display_balance(1234) # => Works

# Test: Withdraw
#puts checking_account.withdraw # => wrong number of arguments (0 for 2) (ArgumentError)
#puts checking_account.withdraw(500) # => wrong number of arguments (1 for 2) (ArgumentError)
#puts checking_account.withdraw(12345, 500) # => Access denied: incorrect PIN.
#puts checking_account.withdraw(1234, 500) # => Works

# Test: Try and access private methods
#puts checking_account.pin # => private method `pin' called for #<Account> (NoMethodError)
#puts checking_account.pin_error # => private method `pin_error' called for #<Account> (NoMethodError)

# Test: Try and access @instance_variables
#puts pin # => undefined local variable or method `pin' for main:Object (NameError)
#puts balance # => undefined local variable or method `balance' for main:Object (NameError)

#13

Avoid using variable names the same as your methods.

def display_balance(pin_number)
    if pin_number == pin

    end
end

The method puts the value, so this call does not need to be printed.


#14

Good advice, will do… It could become messy and hard to figure out otherwise. :confused:


#15

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.