Ruby: Reducing Repetition - ruby

I'm a beginner and wrote a script for the following question below in ruby. I read that repetition isn't recommended and would like to reduce the repetition of if, elsif, else statements but can't seem to find a way.
Old-school Roman numerals. In the early days of Roman numer- als, the Romans didn’t bother with any of this new-fangled sub- traction “IX” nonsense. No sir, it was straight addition, biggest to littlest—so 9 was written “VIIII,” and so on. Write a method that when passed an integer between 1 and 3000 (or so) returns a string containing the proper old-school Roman numeral. In other words, old_roman_numeral 4 should return 'IIII'. Make sure to test your method on a bunch of different numbers. Hint: Use the inte- ger division and modulus methods on page 37.
For reference, these are the values of the letters used:
I =1 V=5 X=10 L=50 C=100 D=500 M=1000
Here is my script...
puts "What is your number?"
n = gets.chomp
num = n.to_i
number = ""
l = n.length
i = 0
while true
if num > 3000
puts "Enter another number."
elsif l == 0
break
else
if l == 4
number += "M" * n[i].to_i
l -= 1
i += 1
elsif l == 3
if 1 <= n[i].to_i && n[i].to_i <= 4
number += "C" * n[i].to_i
elsif n[i].to_i == 5
number += "D"
elsif 6 <= n[i].to_i && n[i].to_i <= 9
number += "D" + "C" * (n[i].to_i - 5)
end
l -= 1
i += 1
elsif l == 2
if 1 <= n[i].to_i && n[i].to_i <= 4
number += "X" * n[i].to_i
elsif n[i].to_i == 5
number += "L"
elsif 6 <= n[i].to_i && n[i].to_i <= 9
number += "L" + "X" * (n[i].to_i - 5)
end
l -= 1
i += 1
else
if 1 <= n[i].to_i && n[i].to_i <= 4
number += "I" * n[i].to_i
elsif n[i].to_i == 5
number += "V"
elsif 6 <= n[i].to_i && n[i].to_i <= 9
number += "V" + "I" * (n[i].to_i - 5)
end
l -= 1
i += 1
end
end
end

This doesn't use integer division or modulus, but it might be instructive.
puts "What is your number?"
input = gets.to_i
numerals = {
1000 => "M",
500 => "D",
100 => "C",
50 => "L",
10 => "X",
5 => "V",
1 => "I"
}
digits = []
numerals.each do |n, digit|
while input >= n
digits << digit
input = input - n
end
end
puts digits.join

Another way, that builds a string, as #sawa suggested, rather than constructing an array and then using join:
numerals = {
1000 => "M",
500 => "D",
100 => "C",
50 => "L",
10 => "X",
5 => "V",
1 => "I"
}
input = 9658
numerals.each_with_object('') do |(n, digit),str|
nbr, input = input.divmod(n)
str << digit*nbr
end
#=> "MMMMMMMMMDCLVIII"

Related

Algorithm Challenge number formatting problem

Invoice numbers are numeric only with any number of digits. To format one correctly, group the digits in group of three plus a group of any remainder, but never leave one digit by itself, unless it's a one digit number. Eg these are all correct formatting
123
12-34
6
783-907-23-45
And these are not
123-4
98-456
There's one more catch user input is passed directly to the function and you never know what characters users might type. Ignore any part of the input that is not digit
Invoice.format_number should always return a string
module Invoice
def self.format_number(str)
return ""
end
end
puts Invoice.format_number("ab1234")
What I have tried
1st approach
arr = []
str.chars.each do |elem|
val = elem =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/
arr << elem if val == 0
end
num_of_digits = arr.length
pairs_of_two = 0
pairs_of_three = 0
if num_of_digits > 5
while num_of_digits > 0 do
break if num_of_digits <= 3
if num_of_digits >= 3 && (num_of_digits % 3 == 0 || num_of_digits % 3 == 2)
pairs_of_three += 1
num_of_digits -= 3
elsif num_of_digits % 2 == 0 || num_of_digits % 2 == 1
pairs_of_two += 1
num_of_digits -= 2
end
end
end
2nd approach
arr = []
str.chars.each do |elem|
val = elem =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/
arr << elem if val == 0
end
len = arr.length - 1
if arr.length > 4
str = ""
i = 0
while i < len do
if arr[i..i+3].length == 4
str << arr[i..i+2].join + "-"
i += 3
elsif arr[i..i+2].length == 3
str << arr[i..i+1].join + "-"
i += 2
elsif arr[i..i+1].length == 2
str << arr[i..i+1].join
i += 2
elsif !arr[i].nil?
str << arr[i]
i += 1
end
end
puts str
else
if arr.length <= 3
puts arr.join
else
puts arr[0..1].join + "-" + arr[2..3].join
end
end
But none of them is correct
Here is the function invoice_number in python
def invoice_number(invoice):
s = ''.join(x for x in invoice if x <= '9' and x >= '0')
n = len(s)
if n <= 3:
return s
w = ''
i = 0
while i + 3 <= n:
for j in range(0, 3):
w += s[i + j]
i += 3
w += ('-')
m = n - i
if m == 0: return w[:-1]
if m == 1: return w[:m-3] + '-' + s[-2:]
return w + s[i:]
Testing
print(invoice_number('1234567'))
print(invoice_number('12345678'))
print(invoice_number('abc123456789'))
print(invoice_number('1234abc5678xyz9foobar'))
123-45-67
123-456-78
123-456-789
123-456-789
Eliminating non-digits is easy with re. For your format, the key is to figure our the "right" splitting indices.
Here is a try:
import re
def splits(n, k):
idx = [(i, min(n, i+k)) for i in range(0, n, k)]
if len(idx) > 1:
(a, b), (c, d) = idx[-2:]
if d - c < 2:
idx[-2:] = [(a, b - 1), (c - 1, d)]
return idx
def myformat(s):
s = re.sub(r'[^0-9]+', '', s)
parts = [s[a:b] for a, b in splits(len(s), 3)]
return '-'.join(parts)
Tests:
>>> myformat('123')
123
>>> myformat('1234')
12-34
>>> myformat('6')
6
>>> myformat('7839072345')
783-907-23-45
As the question was asked for ruby, adding solution for ruby. (The inspiration of the code is mostly from #yuri answer)
def format_invoice(invoice)
# only numbers are allowed
invoice = invoice.tr("^0-9","")
#puts invoice
return invoice if(invoice.length <= 3)
formatted_invoice = ''
i = 0
# Loop to divide the invoice in group of 3
while i + 3 <= invoice.length do
for j in 0..2 do
formatted_invoice += invoice[i + j]
end
i += 3
formatted_invoice += ('-')
end
m = invoice.length - i
return formatted_invoice[0..-2] if m == 0
return formatted_invoice[0..m-4] + '-' + invoice[-2..-1] if m == 1
return formatted_invoice + invoice[i..-1]
end
Testing
puts format_invoice('abc1') # 1
puts format_invoice('abc123') # 123
puts format_invoice('abc123A4') # 12-34
puts format_invoice('1234567') # 123-45-67
puts format_invoice('12345678') # 123-456-78
puts format_invoice('abc123456789') # 123-456-789
puts format_invoice('1234a#c5678xyz9foobar') # 123-456-789

How can I reduce my if statements?

Going through Chris Pine's Learn To Program and working on the number to roman numeral conversion project. The code below works, however it's pretty ugly w/ all those if (and end) statements. However, when I use elsif the program doesn't respond (appears to freeze up). Any thoughts would be helpful!
def calc input
roman_numeral = ''
while true
if input >= 1000
roman_numeral += 'M' * (input / 1000)
input = input - (1000 * (input / 1000))
if input <= 999 || input >= 500
roman_numeral += 'D' * (input / 500)
input = input - (500 * (input / 500))
if input <= 499 || input >= 100
roman_numeral += 'C' * (input / 100)
input = input - (100 * (input / 100))
if input <= 99 || input >= 50
roman_numeral += 'L' * (input / 50)
input = input - (50 * (input / 50))
if input <= 49 || input >= 10
roman_numeral += 'X' * (input / 10)
input = input - (10 * (input / 10))
if input <= 9 || input >= 5
roman_numeral += 'V' * (input / 5)
input = input - (5 * (input / 5))
if input <= 4 || input >= 1
roman_numeral += 'I' * (input / 1)
input = input - (1 * (input / 1))
puts roman_numeral
break
end
end
end
end
end
end
end
end
end
puts 'Give me a number, any number:'
input = gets.chomp.to_i
calc(input)
It's convenient to use the method Enumerable#find with an array:
ARR = [[1000,'M'], [ 500,'D'], [100,'C'], [50,'L'], [10,'X'], [5,'V'], [1,'I']]
def which(input)
ARR.find { |v,_| input >= v }
end
which(2) #=> [1, "I"]
which(7) #=> [5, "V"]
which(17) #=> [10, "X"]
which(77) #=> [50, "L"]
which(777) #=> [500, "D"]
which(7777) #=> [1000, "M"]
Assuming you are converting an integer to a roman numeral, consider making use of the method Fixnum#divmod. Suppose the integer were 2954 and you've already determined that there are two "M"'s and one "D" (so the beginning of your roman numeral string is "MMD"), and that 454 is left over. Then:
c, rem = 454.divmod(100)
#=>[4, 54]
c #=> 4
rem #=> 54
tells you there are four "C"'s with 54 left over.
Four "C"'s are written "CD" (not "CCCC"), however, so you may want to use a hash such as the following:
REP = {..., "C"=>["C", "CC", "CCC", "CD"], ...}
to convert the number of "C"'s to a roman numeral. Here you would append REP["C"][4-1] #=> "CD" to "MMD": "MMD" << "CD" #=> "MMDCD".
The answer from Cary Swoveland is an excellent way to decrease your if block nesting.
His answer tells you which numeral comes next, but not how many (as in your code). A natural way to tie it together is with a recursive function call:
class Romans
def self.calc(input, acc = "")
raise ArgumentError.new("Roman Numerals must be positve") if input < 0
raise ArgumentError.new("Roman Numerals must be integers") if ! input.is_a? Integer
return acc if input == 0
amount, numeral = which(input)
acc += numeral
input -= amount
calc(input, acc)
end
##ARR = [[1000,'M'], [ 500,'D'], [100,'C'], [50,'L'], [10,'X'], [5,'V'], [1,'I']]
def self.which(input)
##ARR.find { |v,_| input >= v }
end
end
In use:
pry(main)> (1..10).each{|i| puts "#{i}=> #{Romans.calc(i)}"}
1=> I
2=> II
3=> III
4=> IIII
5=> V
6=> VI
7=> VII
8=> VIII
9=> VIIII
10=> X
pry(main)> [Random.rand(1..100000)].each{|i| puts "#{i}=> #{Romans.calc(i)}"}
63124=> MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMCXXIIII
Be aware that ruby doesn't have TCO, and so will blow the stack with large enough numbers- but if you need the Roman numeral version of 8 million, you might want to make up some new letters.
Here's one that uses string multiplication. For example: ('M' * 3) = 'MMM'
def to_roman(number)
raise 'Must use positive numbers.' if number <= 0
roman = ''
roman << 'M' * (number / 1000)
roman << 'D' * (number % 1000 / 500)
roman << 'C' * (number % 500 / 100)
roman << 'L' * (number % 100 / 50)
roman << 'X' * (number % 50 / 10)
roman << 'V' * (number % 10 / 5)
roman << 'I' * (number % 5 / 1)
roman
end
puts to_roman(1234) # <-- test
Reference: Learn to Program 2nd by Chris Pine

Is there a better way to write multiple OR statements in an if-statement?

def get_string(no_of_times)
1.upto(no_of_times) do
string_input = gets.chomp
count_holes(string_input)
end
end
def count_holes(word)
count = 0
word.each_char do |char|
if char == "A" || char == "D" || char == "O" || char == "P" || char == "Q" || char == "R"
count += 1
elsif char == "B"
count += 2
end
end
$arr_of_holes << count
end
test_cases = gets.chomp.to_i
$arr_of_holes = []
get_string(test_cases)
puts $arr_of_holes
Hi all. I do not like the long condition in if statement while iterating over each character. So i wanted to ask you all if there is a better way to do this in ruby.
Thanks
This can be done with a case selection, as multiple terms can be supplied to each when:
case char
when "A", "D", "O", "P", "Q", "R"
count += 1
when "B"
count += 2
end
You can use Array#include?:
if %q{A D O P Q R}.include? char
count += 1
elsif char == "B"
count += 2
end
Alternative way using Hash:
def count_holes(word)
holes = {
'A' => 1,
'D' => 1,
'O' => 1,
'P' => 1,
'Q' => 1,
'B' => 2,
}
count = word.chars.map { |char| holes.fetch(char, 0) }.inject :+
$arr_of_holes << count
end
Slightly more compact than nacyot's answer:
count += case char
when "B" then 2
when "A", "D", "O".."R" then 1
else 0
end
The else line may not be required if there is not such case.
One more way:
word = "BROADLY"
"ADOPQR".each_char.reduce(0) { |t,c| t + word.count(c) } + 2*word.count("B")
#=> 6

Remainder - My code is not correct, but why is it working?

Yesterday I completed an exercise (9.5) from the book "Learn to Program" from Chris Pine.
It's an integer to Old-school Roman numerals converter.
This is how I did it:
def old_roman_numeral number
roman_number = ""
while number != 0
if number % 1000 == 0
number -= 1000
roman_number += "M"
next
elsif number % 500 == 0
number -= 500
roman_number += "D"
next
elsif number % 100 == 0
number -= 100
roman_number += "C"
next
elsif number % 50 == 0
number -= 50
roman_number += "L"
next
elsif number % 10 == 0
number -= 10
roman_number += "X"
next
elsif number % 5 == 0
number -= 5
roman_number += "V"
next
else
number -= 1
roman_number += "I"
end
end
roman_number.reverse
end
puts "Please enter any number and I convert it to"
puts "Old-school Roman numerals."
puts
num = gets.chomp.to_i
puts "Your number #{num} converted to Old-school Roman is:"
puts (old_roman_numeral num)
When I run the script, it outputs the correct Roman numerals.
For example 1200 => MCC
But, when I woke up today the first thing I thought was, this can't be right!
The remainder of 1200 % 1000 is 200 and not 0!
But why is the output MCC and not CCCCCCCCCCCC???
If you trace through the program, it's actually matching the % 100 == 0 twice first, getting CC, and leaving you with 1000. Then it matches % 1000 == 0, leaving CCM. Finally it reverses the string, leaving you with MCC.
Side comment: Interesting approach to the problem as I probably would have used a bunch of >= comparisons building the string in forward order with special cases for the 'subtraction' parts (IV or IX). Although on second read, this solution appears to output IIII and not IV, so the special cases are moot.
the code calculates the digits in reverse order. i.e.
you get a C first, then another C and in the third iteration of the loop, you get the M.
in the end, this line of code:
roman_number.reverse
reverses CCM to MCC and thus you get the result you actually get.
to better understand what happens, you can change your code as follows:
if number % 1000 == 0
number -= 1000
roman_number += "M"
next
becomes:
if number % 1000 == 0
number -= 1000
roman_number += "M"
puts "number " + number.to_s
puts "roman_number " + roman_number
next
do this for every if-block. that way you will see what happens in each step.
Not an answer, but just for note, here is a method with similar purpose, taken from my personal library:
class Numeric
RomanNumerals = {
1000 => "m", 900 => "cm", 500 => "d", 400 => "cd",
100 => "c", 90 => "xc", 50 => "l", 40 => "xl",
10 => "x", 9 => "ix", 5 => "v", 4 => "iv", 1 => "i"
}
def roman
return to_s unless 0 < self and self < 4000 # Cannot be romanized
s, r = "", self
RomanNumerals.each{|d, c| q, r = r.divmod(d); s.concat(c*q)}
s
end
end

Why doesn't my conversion code for roman numbers ('mcmxcix' for e.g) to real numbers work?

I want to convert Roman numerals, such as "mcmxcix", to arabic integers like "1999".
My code looks like:
#~ I = 1 V = 5 X = 10 L = 50
#~ C = 100 D = 500 M = 1000
def roman_to_integer roman
len = roman.length
x = 1
while x <= len
arr = Array.new
arr.push roman[x]
x += 1
end
num = 0
arr.each do |i|
if i == 'I'
num += 1
elsif i == 'V'
num += 5
elsif i == 'X'
num += 10
elsif i == 'L'
num += 50
elsif i == 'C'
num += 100
elsif i == 'D'
num += 500
elsif i == 'M'
num += 1000
end
end
num
end
puts(roman_to_integer('MCMXCIX'))
The output is 0, but I don't understand why?
Ruby doesn't have a post-increment operator. When it sees ++ it interprets that as one infix + followed by one prefix (unary) +. Since it expects an operand to follow after that, but instead finds the keyword end, you get a syntax error.
You need to replace x++ with x += 1.
Furthermore note that x isn't actually in scope inside the roman_to_integer method (which isn't a syntax error, but nevertheless wrong).
Additionally you'll have to replace all your ifs except the first with elsifs. The way you wrote it all the ifs are nested, which means that a) you don't have enough ends and b) the code doesn't have the semantics you want.
You are missing a closing parentheses so
puts(roman_to_integer('mcmxcix')
should be
puts roman_to_integer('mcmxcix')
or
puts(roman_to_integer('mcmxcix'))
The arr keeps getting annihilated in your while loop, and it is not in the scope outside of the loop. Move the following line above the while statement:
arr = Array.new

Resources