Ruby method conditions - ruby

The instructions for my program are to make a method that takes a string and returns dashes around any odd numbers, but with the exception that the end result may not begin or end with a dash "-". For example, if I enter "95601137", it should return
"9 - 5 - 6 0 - 1 - 1 - 3 - 7"
Here's the code:
def dashes(number)
string = number.to_s
i=0
final = []
while i<string.length
digit = string[i].to_i
if ((digit)%2 != 0) && (i==0) && (string.length == 1) && ((string.length - 1) == 0)
final.push(string[i])
elsif ((digit)%2 != 0) && (i==0) && (string.length != 1) && ((string.length - 1) > 0)
final.push(string[i] + "-")
elsif ((digit)%2 != 0) && (i>0) && (i!=(string.length - 1)) && ((string.length != 1))
final.push("-" + string[i] + "-")
elsif ((digit)%2 != 0) && (i!=0) && (i==(string.length - 1))
final.push("-" + string[i])
else final.push(string[i])
end
i+=1
end
return final
end
puts("Give me any number.")
answer = gets
puts dashes(answer)
My program has two issues:
When I enter 9, it returns "9-". How come?
When I enter a string that ends with an odd number, it puts a dash at the end, which it's not expected. I thought I made my if conditions strict, too.

I'd like to propose another solution:
def dashes(num)
str = num.to_s
dashed_arr = str.chars.map.with_index do |digit, index|
next digit if digit.to_i.even?
if index.zero? # is it first digit?
"#{digit} -"
elsif index == str.length - 1 # is it last digit?
"- #{digit}"
else
"- #{digit} -"
end
end
dashed_arr.join(' ').gsub('- -', '-')
end
puts dashes(95601137)
puts dashes(909)
# => "9 - 5 - 6 0 - 1 - 1 - 3 - 7"
# => "9 - 0 - 9"
Let's review it step by step:
str = num.to_s - convert our number to a string representation, i.e. "95601137"
str.chars - split our string by chars. We get an array of chars, like this: ["9", "5", "6", "0", "1", "1", "3", "7"], because we want to iterate them, one by one.
map takes one array and converts it to another array by the code supplied in do ... end.
For example, let's take this code:
[1, 2, 3].map do |num|
num * 2
end
# => [2, 4, 6]
If you need also an index of your element, you can similarly use .map.with_index (index starts from zero):
[1, 2, 3].map.with_index do |num, index|
num * index # index takes values 0, 1, 2
end
# => [0, 2, 6]
So, in the block in code above, we have each digit from our number as digit and its 0-based position as index.
next digit if digit.to_i.even?. We don't need even digits, so we can skip them. .to_i is needed to convert digit to integer, so we can query is it even or odd. next digit just returns current digit and moves execution to next digit.
Now we're left only with odd digits, so we just need to put dashes correctly, depending on the digit position: if it's a first digit, append a dash, if it's a last digit then put the dash in front of the digit, otherwise just surround digit with dashes:
if index.zero? # first digit, index is zero-based
"#{digit} -"
elsif index == str.length - 1 # last digit
"- #{digit}"
else
"- #{digit} -"
end
We save intermediate result in dashed_arr variable to make code readable. Right now it contains the following: ["9 -", "- 5 -", "6", "0", "- 1 -", "- 1 -", "- 3 -", "- 7"]. As you can see, we're almost done, we just need to connect all the elements of array back to string.
dashed_arr.join(' '). Join back elements of array into a string with single space as a separator. We get this string: 9 - - 5 - 6 0 - 1 - - 1 - - 3 - - 7. Hmm, it seems we need to delete some consecutive dashes.
Let's do it with gsub: dashed_arr.join(' ').gsub('- -', '-'). gsub just searches all occurrences of first string and replaces them with the second string, which is exactly what we need: 9 - 5 - 6 0 - 1 - 1 - 3 - 7.
We're done! I hope it was also fun for you as for me.

One more implementation, inspired by Cary Swoveland's first implementation:
str = '95601137'
separators = str.each_char.map(&:to_i).each_cons(2).map do |pair|
pair.any?(&:odd?) ? ' - ' : ' '
end
str.chars.zip(separators).join
# => "9 - 5 - 6 0 - 1 - 1 - 3 - 7"

Here are two ways, for:
str = "95601137"
Use an enumerator
With this method you would use String#each_char to create an enumerator to which you would send the methods Enumerator#next and Enumerator#peek:
enum = str.each_char
s = ''
loop do
c = enum.next
s << c
n = enum.peek
s << ((c.to_i.odd? || n.to_i.odd?) ? ' - ' : ' ')
end
s #=> "9 - 5 - 6 0 - 1 - 1 - 3 - 7"
The key here is that when the enumerator is at the last element ('7') peek raises a StopIteration exception. The exception is handled by Kernel#loop by breaking out of the loop.
Use String#gsub
We can use gsub three times, each with a regular expression, to:
put the dashes in the correct locations
space as desired around the dashes
space as desired between even digits
r = /
(\A\d) # match beginning of string followed by a digit, save in group 1
| # or
(\d\z) # match a digit followed by end of string, save in group 2
| # or
(\d) # match a digit, save in capture group 3
/x # extended mode
str = "95601137"
str.gsub(r) do |s|
if $1
$1.to_i.odd? ? "#{$1}-" : $1
elsif $2
$2.to_i.odd? ? "-#{$2}" : $2
elsif $3
$3.to_i.odd? ? "-#{$3}-" : $3
else
raise ArgumentError, "'#{s}' is not a digit"
end
end.gsub(/-+/, ' - ').gsub(/(\d)(\d)/,'\1 \2')
#=> "9 - 5 - 6 0 - 1 - 1 - 3 - 7"

Replace:
answer = gets
with:
answer = gets.strip
Then, you will get the correct output for input: 9
Because, when you give 9 as input and hit enter, your input is actually: "9\n" which has length 2. That's why it was printing 9-.
Using: gets.strip you just strip out the \n.
To remove the last - (if any), you can use chomp:
final_result = final.join('').chomp('-')
And, to remove the first - (if any), you can use this logic:
if final_result[0] == '-'
final_result[1..-1]
else
final_result
end
Here is the full working version of the code (keeping as much as of your code):
def dashes(number)
string = number.to_s
i = 0
final = []
while i < string.length
digit = string[i].to_i
if ((digit)%2 != 0) && (i==0) && (string.length == 1) && ((string.length - 1) == 0)
final.push(string[i])
elsif ((digit)%2 != 0) && (i==0) && (string.length != 1) && ((string.length - 1) > 0)
final.push(string[i] + "-")
elsif ((digit)%2 != 0) && (i>0) && (i!=(string.length - 1)) && ((string.length != 1))
final.push("-" + string[i] + "-")
elsif ((digit)%2 != 0) && (i!=0) && (i==(string.length - 1))
final.push("-" + string[i])
else
final.push(string[i])
end
i += 1
end
final_result = final.join('').gsub('--', '-').chomp('-')
puts "final_result: #{final_result.inspect}"
if final_result[0] == '-'
final_result[1..-1]
else
final_result
end
end
puts("Give me any number.")
answer = gets.strip
puts dashes(answer)
# > Give me any number.
# > 95601137
# > "9-5-60-1-1-3-7"

A naive implementation, there's probably some smarter ways to do it:
def dashes(number)
number # => 95601137
.to_s # => "95601137"
.chars # => ["9", "5", "6", "0", "1", "1", "3", "7"]
.map(&:to_i) # => [9, 5, 6, 0, 1, 1, 3, 7]
.map {|i| i.odd? ? "-#{i}-" : i } # => ["-9-", "-5-", 6, 0, "-1-", "-1-", "-3-", "-7-"]
.join # => "-9--5-60-1--1--3--7-"
.gsub(/^-|-$/, '') # => "9--5-60-1--1--3--7"
.gsub('--', '-') # => "9-5-60-1-1-3-7"
end

Related

Codewars: "Return or rotate": why isn't my attempted solution working?

These were the instructions given on Codewars (https://www.codewars.com/kata/56b5afb4ed1f6d5fb0000991/train/ruby):
The input is a string str of digits. Cut the string into chunks (a chunk here is a substring of the initial string) of size sz (ignore the last chunk if its size is less than sz).
If a chunk represents an integer such as the sum of the cubes of its digits is divisible by 2, reverse that chunk; otherwise rotate it to the left by one position. Put together these modified chunks and return the result as a string.
If
sz is <= 0 or if str is empty return ""
sz is greater (>) than the length of str it is impossible to take a chunk of size sz hence return "".
Examples:
revrot("123456987654", 6) --> "234561876549"
revrot("123456987653", 6) --> "234561356789"
revrot("66443875", 4) --> "44668753"
revrot("66443875", 8) --> "64438756"
revrot("664438769", 8) --> "67834466"
revrot("123456779", 8) --> "23456771"
revrot("", 8) --> ""
revrot("123456779", 0) --> ""
revrot("563000655734469485", 4) --> "0365065073456944"
This was my code (in Ruby):
def revrot(str, sz)
# your code
if sz > str.length || str.empty? || sz <= 0
""
else
arr = []
while str.length >= sz
arr << str.slice!(0,sz)
end
arr.map! do |chunk|
if chunk.to_i.digits.reduce(0) {|s, n| s + n**3} % 2 == 0
chunk.reverse
else
chunk.chars.rotate.join
end
end
arr.join
end
end
It passed 13/14 test and the error I got back was as follows:
STDERR/runner/frameworks/ruby/cw-2.rb:38:in `expect': Expected: "", instead got: "095131824330999134303813797692546166281332005837243199648332767146500044" (Test::Error)
from /runner/frameworks/ruby/cw-2.rb:115:in `assert_equals'
from main.rb:26:in `testing'
from main.rb:84:in `random_tests'
from main.rb:89:in `<main>'
I'm not sure what I did wrong, I have been trying to find what it could be for over an hour. Could you help me?
I will let someone else identify the problem with you code. I merely wish to show how a solution can be speeded up. (I will not include code to deal with edge cases, such as the string being empty.)
You can make use of two observations:
the cube of an integer is odd if and only if the integer is odd; and
the sum of collection of integers is odd if and only if the number of odd integers is odd.
We therefore can write
def sum_of_cube_odd?(str)
str.each_char.count { |c| c.to_i.odd? }.odd?
end
Consider groups of 4 digits in the last example, "563000655734469485".
sum_of_cube_odd? "5630" #=> false (so reverse -> "0365")
sum_of_cube_odd? "0065" #=> true (so rotate -> "0650")
sum_of_cube_odd? "5734" #=> true (so rotate -> "7345")
sum_of_cube_odd? "4694" #=> true (so rotate -> "6944")
so we are to return "0365065073456944".
Let's create another helper.
def rotate_chars_left(str)
str[1..-1] << s[0]
end
rotate_chars_left "0065" #=> "0650"
rotate_chars_left "5734" #=> "7345"
rotate_chars_left "4694" #=> "6944"
We can now write the main method.
def revrot(str, sz)
str.gsub(/.{,#{sz}}/) do |s|
if s.size < sz
''
elsif sum_of_cube_odd?(s)
rotate_chars_left(s)
else
s.reverse
end
end
end
revrot("123456987654", 6) #=> "234561876549"
revrot("123456987653", 6) #=> "234561356789"
revrot("66443875", 4) #=> "44668753"
revrot("66443875", 8) #=> "64438756"
revrot("664438769", 8) #=> "67834466"
revrot("123456779", 8) #=> "23456771"
revrot("563000655734469485", 4) #=> "0365065073456944"
It might be slightly faster to write
require 'set'
ODD_DIGITS = ['1', '3', '5', '7', '9'].to_set
#=> #<Set: {"1", "3", "5", "7", "9"}>
def sum_of_cube_odd?(str)
str.each_char.count { |c| ODD_DIGITS.include?(c) }.odd?
end

ruby access non-vowels of a string in condition line without arrays

I need to turn a string that contains (among other letters) a sequence of 3 letters consisting of a non-vowel, an "o" and the same non-vowel again
into
a string that contains (other letters and) only that non-vowel.
like
"kok" #=> "k"
"mom" #=> "m"
"lol" #=> "l"
"kokmomloljk" #=> "kmljk"
I would like my code to be as compact as possible and only use string methods.
str.each_char { | i | if i == /[^aeiou]/ and i == str[i.index + 2] and str[i.index + 1] == "o"
str = str.delete(str.slice(str[i.index + 1], 2))
end
}
The output is the unchanged string. Thank you in advance.
R = /
([^aeiou]) # match a consonant in capture group 1
o # match an 'o'
\1 # match the contents of capture group 1
/x # free-spacing regex definition mode
def my_method(str)
str.gsub(R,'\1')
end
my_method "my dog kok has fleas"
#=> "my dog k has fleas"
my_method "much momentum"
#=> "much mentum"
my_method "'plolly' is not a word"
#=> "'plly' is not a word"
my_method "abkokcmomloljkde"
#=> "abkcmljkde"
my_method "bub"
#=> "bub"
I was wondering if you could do this in a more functional non-destructieve way with map and, yes you can, but not more compact than the other answers:
str = "iskakmomlol"
VOWEL = /[aeiou]/
RESONANT = /[^aeiou]/
str.chars.map.with_index { |c, i|
prevb, prev, nxt, scnd = str[i - 2], str[i - 1], str[i + 1], str[i + 2]
if i > str.length - 1 || i == 0 then c
elsif c =~ RESONANT && nxt =~ VOWEL && c == scnd then c
elsif c =~ VOWEL && prev =~ RESONANT && nxt =~ RESONANT
elsif c =~ RESONANT && prevb == c && prev =~ VOWEL
else c
end
}.compact.join # "iskmljk"
Actually, this can be shorter:
R = /([^aeiou])[aeiou]\1/
str.chars.map.with_index { |c, i|
c unless str[i-1..i+1][R] || str[i-2..i][R]
}.compact.join # "iskmljk"
i figured since the "o" is a fix character in the sequence to be accepted, i can just go through with a count var and see if the characters next to it are equal to each other and non-vowels. also i found that slice can be also passed two parameters so that it slices starting at an offset and stopping after the given length.
index = 0
while index < str.length
index = index + 1
if str[index] == "o" and str[index-1] == str[index+1] and str[index-1] != /[^aeiou]/
str.slice!(index, 2)

Longest Run Program In Ruby

I have a school assignment where i have to find the longest run of adjacent equal characters in a given string with Ruby. My program works fine without the last loop, but once i added it gave me the error:
(repl):47: syntax error, unexpected keyword_end
(repl):53: syntax error, unexpected end-of-input, expecting keyword_end
puts longestRun
^
Here is my Code
puts 'What is your string?'
givenString = gets.chomp
def maxBlock(str)
maxRun = 0
currentRun = 1
characterCounter = 1
if str.length == 0
maxRun = 0
#If no input, longest run is zero
elsif str.length == 1
maxRun = 1
#If string is one character, longest run is 1
elsif str.length == 2 and str[characterCounter] != str[characterCounter + 1]
maxRun = 1
#if string is two chars and they do not equal, longest run is 1
elsif str.length == 3 and str[0] != str[1] and str[1] != str[2]
maxRun = 1
#if string is three chars and they do not equal, longest run is 1
else
str.each_char do|st|
#Go through each char, compare it to the next, find longest run
if st == str[characterCounter]
currentRun++
if currentRun > maxRun
maxRun = currentRun
end
else
currentRun = 1
end
characterCounter++
end
end
end
longestRun = maxBlock(givenString)
puts longestRun
EDIT: I am a highschool student, and only have a base knowledge of programming.
EDIT: I just made a few stupid mistakes. I appreciate everyone's help. Here is my working program without the use of anything too complicated.
puts 'What is your string?'
givenString = gets.chomp
def maxBlock(str)
maxRun = 0
currentRun = 1
characterCounter = 0
if str.length == 0
maxRun = 0
#If no input, longest run is zero
elsif str.length == 1
maxRun = 1
#If string is one character, longest run is 1
elsif str.length == 2 and str[characterCounter] != str[characterCounter + 1]
maxRun = 1
#if string is two chars and they do not equal, longest run is 1
elsif str.length == 3 and str[0] != str[1] and str[1] != str[2]
maxRun = 1
#if string is three chars and they do not equal, longest run is 1
else
characterCounter += 1
str.each_char do|st|
#Go through each char, compare it to the next, find longest run
if st == str[characterCounter]
currentRun += 1
if currentRun > maxRun
maxRun = currentRun
end
else
currentRun = 1
end
characterCounter += 1
end
end
return maxRun
end
longestRun = maxBlock(givenString)
puts longestRun
String Scans and Sorting
There are algorithms for this, but Ruby offers some nice shortcuts. For example:
def longest_string str
str.scan(/((\p{Alnum})\2+)/).collect { |grp1, grp2| grp1 }.sort_by(&:size).last
end
longest_string 'foo baaar quuuux'
#=> "uuuu"
This basically just captures all runs of repeated characters, sorts the captured substrings by length, and then returns the last element of the length-sorted array.
Secondary Sorting
If you want to do a secondary sort, such as first by length and then by alphabetical order, you could replace Enumerable#sort_by with the block form of Enumerable#sort. For example:
def longest_string str
str.scan(/((\p{Alnum})\2+)/).
collect { |grp1, grp2| grp1 }.
sort {|a, b| [a.size, a] <=> [b.size, b] }.
last
end
longest_string 'foo quux baar'
#=> "uu"
This is one way you could do it.
str = "111 wwwwwwwwaabbbbbbbbbbb$$$$****"
r = /
(.) # Match any character in capture group 1
\1* # Match the contents of capture group 1 zero or more times
/x # Free-spacing regex definition mode
str.gsub(r).max_by(&:size)
#=> "bbbbbbbbbbb"
I used the form of String#gsub without a second argument or block, as that returns an enumerator that generates the strings matched by the regex. I then chained that enumerator to the method Enumerable#max_by to find the longest string of consecutive characters. In other words, I used gsub merely to generate matches rather than to perform substitutions.
One could of course write str.gsub(/(.)\1*/).max_by(&:size).
Here is a simplified version that should work in all cases:
puts 'What is your string?'
given_string = gets.chomp
def max_block(str)
max_run = 0
current_run = 1
str.each_char.with_index do |st, idx|
if st == str[idx + 1]
current_run += 1
else
current_run = 1
end
max_run = current_run if current_run > max_run
end
max_run
end
longest_run = max_block(given_string)
puts longest_run
You were on the right track but Ruby can make things a lot easier for you. Notice how with_index gets rid of a lot of the complexity. Iterators, oh yeah.
I also changed your method name and variables to camel_case.
Happy coding!

Why am I getting an IndexError

I'm trying to write some code that will take an array of numbers and print a string representation of the range of the numbers.
def rng (arr)
str = arr[0].to_s
idx = 1
arr.each do |i|
next if arr.index(i) == 0
if arr[arr.index(i)-1] == i - 1
unless str[idx - 1] == "-"
str[idx] = "-"
#else next
end
#puts "if statement str: #{str}, idx: #{idx}"
else
str[idx] = arr[arr.index(i)-1].to_s
idx += 1
str[idx] = ","+ i.to_s
end
idx += 1
end
puts "str = #{str} and idx = #{idx}"
end
rng [0, 1, 2, 3, 8] #"0-3, 8"
I get this error:
arrayRange_0.rb:9:in `[]=': index 3 out of string (IndexError)
Can anyone explain why? When I uncomment the else next it works. Not sure why.
When you get that error, str contains the value 0- which is only 2 characters long - therefore it can't be indexed to the position of 3.
Add this line before line 9, which is causing your error:
puts "str = #{str}, idx = #{idx}"
It will output:
str = 0, idx = 1
str = 0-, idx = 3
Here is how you could do it:
def rng(arr)
ranges = []
arr.each do |v|
if ranges.last && ranges.last.include?(v-1)
# If this is the next consecutive number
# store it in the second element
ranges.last[1] = v
else
# Add a new array with current value as the first number
ranges << [v]
end
end
# Make a list of strings from the ranges
# [[0,3], [8]] becomes ["0-3", "8"]
range_strings = ranges.map{|range| range.join('-') }
range_strings.join(', ')
end
p rng [0, 1, 2, 3, 8]
# results in "0-3, 8"
Like the previous answer says, your index is outside of the string

Number of repeats

Write a method that takes in a string and returns the number of letters that appear more than once in the string. You may assume the string contains only lowercase letters. Count the number of letters that repeat, not the number of times they repeat in the string.
I implemented methods and test cases as:
def num_repeats(string)
count = 0
dix = 0
new = ""
while dix < string.length
letter = string[dix]
if !(new.include?(letter))
new = new + "letter"
else
break
end
dix2 = dix + 1
while dix2 < string.length
if letter == string[dix2]
count +=1
break
end
dix2 +=1
end
dix += 1
end
puts(count.to_s)
return count
end
# These are tests to check that your code is working. After writing
# your solution, they should all print true.
puts('num_repeats("abdbc") == 1: ' + (num_repeats('abdbc') == 1).to_s)
# one character is repeated
puts('num_repeats("aaa") == 1: ' + (num_repeats('aaa') == 1).to_s)
puts('num_repeats("abab") == 2: ' + (num_repeats('abab') == 2).to_s)
puts('num_repeats("cadac") == 2: ' + (num_repeats('cadac') == 2).to_s)
puts('num_repeats("abcde") == 0: ' + (num_repeats('abcde') == 0).to_s)
Test results:
1
num_repeats("abdbc") == 1: true
2
num_repeats("aaa") == 1: false
2
num_repeats("abab") == 2: true
2
num_repeats("cadac") == 2: true
0
num_repeats("abcde") == 0: true
For the second test that returned false, what was wrong with my code?
You are appending "letter", rather than the letter variable to new on line 8.
if !(new.include?(letter))
new = new + "letter"
else
#...
end
becomes:
unless new.include?(letter)
new = new + letter
else
#...
end

Resources