How should I refactor this conditional code in ruby? - ruby

How to refactor this function?
def split_description(first_n)
description_lines = description.split "\n"
line_num = description_lines.length
if line_num > first_n
#description_first = to_html(description_lines[0..first_n].join("\n"))
#description_remain = to_html(description_lines[first_n + 1..line_num].join("\n"))
elsif line_num > 1
#description_first = to_html(description_lines[0..first_n].join("\n"))
#description_remain = ''
else
#description_first = ''
#description_remain = ''
end
end
I am a Ruby starter and encounter this rubocup warning: Method has too many lines. [13/10]
The following is whole code url:
https://github.com/RubyStarts3/YPBT-app/blob/master/views_objects/video_info_view.rb

Code
def split_description(description, first_n)
#description_first, #description_remain =
case description.count("\n")
when 0..first_n
[description, '']
else
partition_description(description, first_n)
end.map(&:to_html)
end
def partition_description(description, first_n)
return ['', description] if first_n.zero?
offset = 0
description.each_line.with_index(1) do |s,i|
offset += s.size
return [description[0,offset], description[offset..-1]] if i == first_n
end
end
I've assumed to_html('') #=> '', but if that's not the case the modification is straightforward.
Example
So that we can see the effect of to_html, let's define it thusly.
def to_html(description)
description.upcase
end
description =<<_
It was the best of times
it was the worst of times
it was the age of wisdom
it was the age of fools
_
split_description(description, 0)
#description_first
#=> ""
#description_remain
#=> "IT WAS THE BEST OF TIMES\n..WORST OF TIMES\n..AGE OF WISDOM\n..AGE OF FOOLS\n"
split_description(description, 1)
#description_first
#=> "IT WAS THE BEST OF TIMES\n"
#description_remain
#=> "IT WAS THE WORST OF TIMES\n..AGE OF WISDOM\n..AGE OF FOOLS\n"
split_description(description, 2)
#description_first
#=> "IT WAS THE BEST OF TIMES\nIT WAS THE WORST OF TIMES\n"
#description_remain
#=> "IT WAS THE AGE OF WISDOM\nIT WAS THE AGE OF FOOLS\n"
split_description(description, 3)
#description_first
#=> "IT WAS THE BEST OF TIMES\n..WORST OF TIMES\n..AGE OF WISDOM\n"
#description_remain
#=> "IT WAS THE AGE OF FOOLS\n"
split_description(description, 4)
#description_first
#=> "IT WAS THE BEST OF TIMES\n..WORST OF TIMES\n..AGE OF WISDOM\n..AGE OF FOOLS\n"
#description_remain
#=> ""
Explanation
Firstly, is appears that description is a local variable holding a string. If so, it must be an argument of the method (along with first_n).
def split_description(description, first_n)
We want to assign values to two instance variables, so let's begin by writing
#description_first, #description_remain =
There are really two steps: obtaining the desired strings and then mapping them with to_html. So let's first concentrate on the first step.
We will now condition on the number of lines in the string
case description.count("\n")
First, let's deal with the case where the string contains no newlines
when 0
[description, '']
If the string is empty this will be ['', '']; otherwise it will contain a single string without a newline.
Next, suppose the number of newlines in the string is between 1 and first_n. In this case #description_first is to be the entire string and #description_remain is to be empty.
when 1..first_n
[description, '']
As both when 0 and when 1..first_n return the same two-element array, we can combine them:
when 0..first_n
[description, '']
To get this far, first_n is less than the number of newlines. I've used another method for the case where the number of newlines is greater than first_n.
else
partition_description(description, first_n)
partition_description simply determines the offset into description of the first_nth newline, and then partitions the string accordingly.
Lastly, we need to end the case statement, map the array of two strings returned with to_html and end the method
end.map(&:to_html)
end
As I mentioned earlier, I've assumed to_html('') #=> ''. That seems to me to be the best place do deal with empty strings.
Note that I've dealt with the string directly, rather than splitting the string into lines, manipulating the lines and then rejoining them.

Since it's used or blanked in every condition, initialize the instance variables to blank.
def split_description(first_n)
description_lines = description.split "\n"
line_num = description_lines.length
#description_first = ''
#description_remain = ''
if line_num > first_n
#description_first = to_html(description_lines[0..first_n].join("\n"))
#description_remain = to_html(description_lines[first_n + 1..line_num].join("\n"))
elsif line_num > 1
#description_first = to_html(description_lines[0..first_n].join("\n"))
end
end
I'd also move the logic for description_lines[first_n + 1..line_num].join("\n") to a method like to_html( whatever_that_is( lines, from, to) ) or the like. Then it's not so bad if you repeat the same call and the name will describe what it's doing.

If first_n is always greater than 1 I think you can modify a little the Schwern's answer:
...
#description_first = to_html(description_lines[0..first_n].join("\n")) if line_num > 1
if line_num > first_n
#description_remain = to_html(description_lines[first_n + 1..line_num].join("\n"))
end
end

This should work :
def split_description(description, first_n = 0)
lines = description.each_line
#description_first = to_html(lines.take(first_n).join)
#description_remain = to_html(lines.drop(first_n).join)
end
take and drop replace all your logic, because, as #Cary Swoveland mentionned in a comment :
if you take too much, you end up with the complete array, without error message
if you drop too much, you end ud with an empty array, without error message
Example :
[1,2].take(99) #=> [1, 2]
[1,2].drop(99) #=> []
Also each_line outputs an Array of Strings, with newlines still present. No split, chomp or join("\n") is needed.

Related

Ruby - Remove 1 or 2 character from a string to make it a palindrome

I had a technical test for an entry-level job 2 days ago. It went well apart from the last assessment.
I will go over the assessment with the CTO tomorrow and was hoping I could get help to get my head around this one, so I do not sound clueless.
It was something like this:
Given string as an argument, give us palindrome method that would check if a palindrome of a minimum 3 characters can be created by removing 1 or 2 characters. The string has no special characters, numbers or whitespace basically only letters (eg: str = "abecbea")
If true, print letters removed, if false print "not possible"
"remove 1 or 2 characters" and "print letters removed" is giving me a headache legit
I have tried a lot of different things but for the last 2 days but i am completely stuck!
[EDIT]
Ideas i started with below
def is_palindrome(s)
s == s.reverse && s.length >= 3
end
def possible_palin_by_removing_one_char(s)
array_chars = s.chars
first_char = array_chars[0]
last_char = array_chars[-1]
while first_char < last_char
if first_char != last_char
first_char += 1
last_char -= 1
else
if is_palindrome
puts ????
else
puts "not possible"
end
end
end
end
or
def palindrome?(string)
deleted_chars = []
candidates = 0.upto(string.length-1).map do |index|
array_chars = string.chars
deleted_chars << array_chars.delete_at(index)
array_chars
end
if candidates.any? { |c| c.reverse == c } && string.length >= 3
puts "It is a palindrome with letters '#{deleted_chars.join(",")}' removed !"
# puts deleted_chars
else
if string.length <= 3
puts "String '#{string}' is too short"
else
puts "This is not possible to create palindrome with string '#{string}'"
end
end
end
palindrome?("abcecbae")
I would love someone to help me solve this one
Thanks heaps for your help
Put all chars in an Array (ar = str.chars)
Try all combinations which are 2 less than
the size of the array (so 5 in the example "abcecbae")(ar.combination(5))
Select all combinations which happen to be equal to their reverse
Map the result(s) back from arrays to strings
Similar for 1 removed char.
This task might be a little bit trickier without Ruby's rich standard library support.
Here is how I'd solve this task in the 1st iteration:
Check if the original string is a palindrome, return immediately if it is
Build a list of possible combinations of indices to remove
Iterate over this list, check if removing the chars at given indices makes our input string a palindrome
The trickiest part here is (2), but Enumerable#permutation almost does the job.
So, something like
def palindrome?(s)
s == s.reverse
end
def indices_to_remove(string)
ary = (0..string.size-1).to_a
# Permutations like [0,1] and [1,0] means the same in our context, so we need to remove this duplication.
(ary.map { |x| [x] } + ary.permutation(2).map(&:sort)).uniq
end
def remove_chars(str, *indices)
str.dup.tap do |s|
indices.sort.reverse_each { |i| s[i] = '' }
end
end
def find_palindrome(str)
return if palindrome?(str)
indices_to_remove(str).each do |indices|
new_str = remove_chars(str, *indices)
return "Letters removed: #{indices.inspect}; palindrome: '#{new_str}'" if palindrome?(new_str)
end
end
That's it. Let's see if i works:
pry(main)> find_palindrome("abecbea")
=> "Letters removed: [1, 3]; palindrome: 'aebea'"
It does. Adjust the filtering logic (check palindroms' size as needed) and output if necessary.
Thanks all for your help. With the help of a senior dev, we came up with the below solution
def palindrome_check(str)
length = str.length
# convert to array of chars
chars = str.chars
# check if palindrome by deleting 1 characters
length.times do |i|
char = chars.delete_at(i)
return "Letter '#{char}' has been removed from String '#{str}' to create palindrome" if chars == chars.reverse
# return the array to original condition
chars.insert(i, char)
end
# only do remove 2 characters check if length > 4, as otherwise removing would result in a string less than 3 characters
if length > 4
length.times do |i|
length.times do |j|
# avoid repeating the same checks
next if j <= i
# since j is always greater than i, remove j first to avoid affecting which character is at position i
char_two = chars.delete_at(j)
char_one = chars.delete_at(i)
return "Letters '#{[char_one, char_two].join(' and ')}' has been removed from String '#{str}' to create palindrome" if chars == chars.reverse
# return the array to original condition
chars.insert(i, char_one)
chars.insert(j, char_two)
end
end
end
return "'#{str}' can't be a Palindrome"
end
palindrome_check('string') # 'can't be a Palindrome'
palindrome_check('abcdcfba') # 'f'
palindrome_check('rqubyxyburx') # 'q, x'

Passing nill value to custom method

Can you please tell me why it is passing nil to check method? I am getting error main.rb:5:in `check': undefined method `%' for nil:NilClass (NoMethodError)
my_array = Array.new
$output = String.new
def check(n)
if n%3 == 0
$output = $output + 'Pop '
elsif n.even?
$output = $output + 'Crackle '
elsif n.odd?
$output = $output + 'Snap '
end
end
for x in 1..6
my_array[x] = gets.chomp.to_i
end
my_array.each { |x| check(x) }
puts $output.my_array
The reason you are getting a nil in the beginning of the array is that you are manually setting the keys in the array which creates a hole since arrays are 0 indexed in Ruby:
ary = Array.new
ary[1] = "a"
ary[2] = "b"
ary[3] = "c"
# => [nil, "a", "b", "c"]
While you could salvage this code with:
my_array = Array.new
$output = String.new
def check(n)
if n%3 == 0
$output = $output + 'Pop '
elsif n.even?
$output = $output + 'Crackle '
elsif n.odd?
$output = $output + 'Snap '
end
end
for x in 0..5
my_array[x] = gets.chomp.to_i
end
my_array.each { |x| check(x) }
puts $output.my_array
A more idiomatically correct way to write this in Ruby is:
str = 5.times.map do
n = gets.chomp.to_i
if n%3 == 0
'Pop'
elsif n.even?
'Crackle'
elsif n.odd?
'Snap'
end
end.join(" ")
puts str
for String.new and Array.new are rarely used if ever used. Use blocks instead of methods unless you're planning to reuse it later. In Ruby you can use the methods from Enumerable to both iterate over and transform arrays, hashes, ranges and other types of objects so there rarely is a reason to iterate and modify an external variable like in other languages.
With for x in 0..5 you would then have
t.rb:21:in `<main>': undefined method `my_array' for "":String (NoMethodError)
because my_array is not a method that you can send to $output.
There are many ways to do the same thing in Ruby.
my_array = []
def check(n)
case
when n % 3 == 0
'Pop'
when n.even?
'Crackle'
when n.odd?
'Snap'
else 'boom !' # not necessary in this particular case
end
end
(1..6).each do | i |
print "#{i} Enter a number > "
my_array << gets.to_i
end
puts my_array.collect { |e| check(e) }.join(' ')
Execution :
$ ruby t.rb
1 Enter a number > 44
2 Enter a number > 66
3 Enter a number > 30494
4 Enter a number > 383849
5 Enter a number > 2234
6 Enter a number > 4333
Crackle Pop Crackle Snap Crackle Snap
Don't use global variables, like $output. In the ancient (imperative programming style) languages, it was a common bug to inadvertently modify a variable accessible from anywhere.
The object oriented paradigm has been invented to isolate variables (encapsulated in an
object) to make it more difficult to modify them accidentally.
You could have use an instance variable :
#output = ''
if n%3 == 0
#output << 'Pop '
but beeing defined in the special 'main' object, it is not protected against unwanted access.
chomp is not necessary before to_i, see this post
Use iterators instead of loops. for is imperative style (C, Java), which imposes you to manage
the begin and end indexes. In an object oriented language, you simply send an iterate message to a
collection object, which takes cares of the internal details.
if and case are expressions which return the last computed value. check() returns that value.
Your my_array.each { |x| check(x) } mutates the variable $output and returns no result. In a big program, a later maintenance could insert some processing that modifies $output before you use it (bug).
The functional programming paradigm (Scala, Elixir, Kotlin) tends to use immutable variables and use functions to transform data.
The new my_array.collect { |e| check(e) }.join(' ') iterates over my_array, transforms each element calling the function check(), produces a new (immutable) collection with these transformed elements, which is then transformed by the function join() to produce the final result.
You have
for x in 1..6
my_array[x] = gets.chomp.to_i
end
Which populates the array from indexes 1 through 6, all arrays begin at index 0 so, in your method
my_array.each { |x| check(x) }
The .each method will iterate through each element of the array, starting at 0, which in this case would be nil because you never assigned a value to that index, you could change your range to
for x in 0..6
my_array[x] = gets.chomp.to_i
end
And that would work, remember to use 2 dots and not 3, as
0..6
0...6
are different, the first one is inclusive, the second one is exclusive.
You can check up more about ranges here

Ruby: Reverse method from scratch

I'm working on a coding challenge practice problem that is asking me to put together a method that reverses a string and have come up with:
def reverse(string)
idx = 1
array_s = []
while idx <= string.length
array_s.push(string[string.length - idx])
idx = idx + 1
end
new_string = array_s.join("")
puts new_string
end
When I run the test given at the end,
puts(
'reverse("abc") == "cba": ' + (reverse("abc") == "cba").to_s
)
puts(
'reverse("a") == "a": ' + (reverse("a") == "a").to_s
)
puts(
'reverse("") == "": ' + (reverse("") == "").to_s
)
it seems to show that the strings reverse, but it still prints false. Is there anything I'm missing in the method I wrote? Thanks.
Your function is working as expected, it is the last line that is causing you issues.
puts new_string
puts returns nil, ruby uses implicit returns unless you explicitly say return, so it returns the result from the last line of code, in this case is nil.
If you just do new_string or return new_string, it will work.
Or if you want to print it to the screen and return it as the result, you can do
p new_string
davidhu2000 has answered your question correctly but i would suggest refactoring the code:
f = "forward"
puts f.reverse
# drawrof
of if you really want:
def reverse(string)
return string.reverse
end

changing integers into words ruby without gems

I am trying to change numbers up to 100 from integers into words, but have run into some trouble, can anyone point out what is missing with my code:
def in_words(integer)
numWords = {
0=>"zero",
1=>"one",
2=>"two",
3=>"three",
4=>"four",
5=>"five",
6=>"six",
7=>"seven",
8=>"eight",
9=>"nine",
10=>"ten",
11=>"eleven",
12=>"twelve",
13=>"thirteen",
14=>"fourteen",
15=>"fifteen",
16=>"sixteen",
17=>"seventeen",
18=>"eighteen",
19=>"nineteen",
20=>"twenty",
30=>"thirty",
40=>"fourty",
50=>"fifty",
60=>"sixty",
70=>"seventy",
80=>"eighty",
90=>"ninety",
100=>"one hundred"
}
array = integer.to_s.split('')
new_array = []
numWords.each do |k,v|
array.each do |x|
if x = k
new_array.push(v)
end
end
end
new_array.join('')
end
Right now when I do:
inwords(0)
I get the following:
=>"zeroonetwothreefourfivesixseveneightnineteneleventwelvethirteenfourteenfiftee nsixteenseventeeneighteennineteentwentythirtyfourtyfiftysixtyseventyeightyninetyone hundred"
Edit
I noticed your code iterates through the array a lot of times and uses the = instead of the == in your if statements.
Your code could be more efficient using the Hash's #[] method in combination with the #map method.., here's a one-line alternative:
integer.to_s.split('').map {|i| numWords[i.to_i]} .join ' '
Also, notice that the integer.to_s.split('') will split the array into one-digit strings, so having numbers up to a hundred isn't relevant for the code I proposed.
To use all the numbers in the Hash, you might want to use a Regexp to identify the numbers you have. One way is to do the following (I write it in one line, but it's easy to break it down using variable names for each step):
integer.to_s.gsub(/(\d0)|([1]?\d)/) {|v| v + " "} .split.map {|i| numWords[i.to_i]} .join ' '
# or:
integer.to_s.gsub(/(#{numWords.keys.reverse.join('|')})/) {|v| v + " "} .split.map {|i| numWords[i.to_i]} .join ' '
# out = integer.to_s
# out = out.gsub(/(#{numWords.keys.reverse.join('|')})/) {|v| v + " "}
# out = out.split
# out = out.map {|i| numWords[i.to_i]}
# out = out.join ' '
Edit 2
Since you now mention that you want the method to accept numbers up to a hundred and return the actual number (23 => twenty three), maybe a different approach should be taken... I would recommend that you update your question as well.
def in_words(integer)
numWords = {
0=>"zero",
1=>"one",
2=>"two",
3=>"three",
4=>"four",
5=>"five",
6=>"six",
7=>"seven",
8=>"eight",
9=>"nine",
10=>"ten",
11=>"eleven",
12=>"twelve",
13=>"thirteen",
14=>"fourteen",
15=>"fifteen",
16=>"sixteen",
17=>"seventeen",
18=>"eighteen",
19=>"nineteen",
20=>"twenty",
30=>"thirty",
40=>"fourty",
50=>"fifty",
60=>"sixty",
70=>"seventy",
80=>"eighty",
90=>"ninety",
100=>"one hundred"
}
raise "cannot accept such large numbers" if integer > 100
raise "cannot accept such small numbers" if integer < 0
return "one hundred" if integer == 100
if integer < 20 || integer %10 == 0
numWords[integer]
else
[numWords[integer / 10 * 10], numWords[integer % 10]].join ' '
end
end
the integer / 10 * 10 makes the number a round number (ten, twenty, etc') because integers don't have fractions (so, 23/10 == 2 and 2 * 10 == 20). The same could be achieved using integer.round(-1), which is probably better.
It seems like all you're trying to do is find a mapping from an implicit hash
module NumWords
INT2STR = {
0=>"zero",
1=>"one",
2=>"two",
3=>"three",
4=>"four",
5=>"five",
6=>"six",
7=>"seven",
8=>"eight",
9=>"nine",
10=>"ten",
11=>"eleven",
12=>"twelve",
13=>"thirteen",
14=>"fourteen",
15=>"fifteen",
16=>"sixteen",
17=>"seventeen",
18=>"eighteen",
19=>"nineteen",
20=>"twenty",
30=>"thirty",
40=>"fourty",
50=>"fifty",
60=>"sixty",
70=>"seventy",
80=>"eighty",
90=>"ninety",
100=>"one hundred"
}
module_function
def in_words(integer)
INT2STR[integer]
end
end
The above code separates the hash definition from the method call so that the hash doesn't get recreated every time you call in_words.
You can also use Hash#fetch instead of Hash#[] as Andrey pointed out.
Your test whether x = k is your first problem (in two ways).
Firstly, if x = k means assign the value of k to x and then execute the if block if that value is true (basically anything other than false or nil).
What you should actually be testing is x == k which will return true if x is equal to k.
The second problem is that you converted your number into an array of string representation so you are comparing, for example, if "0" == 0. This won't return true because they are different types.
If you convert it to if x.to_i == k then your if block will be executed and you'll get:
> in_words(0)
=> "zero"
Then you get to move onto the next problem which is that you're looking at your number digit by digit and some of the values you are testing against need two digits to be recognised:
> in_words(10)
=> "zeroone"
You might be in looking at a different question then - or maybe that is the question you wanted answered all along!
Here's another way you might do it:
ONES_TO_TEXT = { 0=>"zero", 1=>"one", 2=>"two", 3=>"three", 4=>"four",
5=>"five", 6=>"six", 7=>"seven", 8=>"eight", 9=>"nine" }
TEENS_TO_TEXT = { 10=>"ten", 11=>"eleven", 12=>"twelve",
13=>"thirteen", 15=>"fifteen" }
TENS_TO_TEXT = { 2=>"twenty", 3=>"thirty", 5=>"fifty", 8=>"eighty" }
def in_words(n)
raise ArgumentError, "#{n} is out-of_range" unless (0..100).cover?(n)
case n.to_s.size
when 1 then ONES_TO_TEXT[n]
when 3 then "one hundred"
else
case n
when (10..19)
TEENS_TO_TEXT.key?(n) ? TEENS_TO_TEXT[n] : ONES_TO_TEXT[n]+"teen"
else
t,o = n.divmod(10)
(TENS_TO_TEXT.key?(t) ? TENS_TO_TEXT[t] : ONES_TO_TEXT[t]+"ty") +
(o.zero? ? '' : "-#{ONES_TO_TEXT[o]}")
end
end
end
Let's try it:
in_words(5) #=> "five"
in_words(10) #=> "ten"
in_words(15) #=> "fifteen"
in_words(20) #=> "twenty"
in_words(22) #=> "twenty-two"
in_words(30) #=> "thirty"
in_words(40) #=> "fourty"
in_words(45) #=> "fourty-five"
in_words(50) #=> "fifty"
in_words(80) #=> "eighty"
in_words(99) #=> "ninety-nine"
in_words(100) #=> "one hundred"
Here the increased complexity may not be justified, but this approach may in fact simplify the calculations when the maximum permitted value of n is much greater than 100.

Find if all letters in a string are unique

I need to know if all letters in a string are unique. For a string to be unique, a letter can only appear once. If all letters in a string are distinct, the string is unique. If one letter appears multiple times, the string is not unique.
"Cwm fjord veg balks nth pyx quiz."
# => All 26 letters are used only once. This is unique
"This is a string"
# => Not unique, i and s are used more than once
"two"
# => unique, each letter is shown only once
I tried writing a function that determines whether or not a string is unique.
def unique_characters(string)
for i in ('a'..'z')
if string.count(i) > 1
puts "This string is unique"
else
puts "This string is not unique"
end
end
unique_characters("String")
I receive the output
"This string is unique" 26 times.
Edit:
I would like to humbly apologize for including an incorrect example in my OP. I did some research, trying to find pangrams, and assumed that they would only contain 26 letters. I would also like to thank you guys for pointing out my error. After that, I went on wikipedia to find a perfect pangram (I wrongly thought the others were perfect).
Here is the link for reference purposes
http://en.wikipedia.org/wiki/List_of_pangrams#Perfect_pangrams_in_English_.2826_letters.29
Once again, my apologies.
s = "The quick brown fox jumps over the lazy dog."
.downcase
("a".."z").all?{|c| s.count(c) <= 1}
# => false
Another way to do it is:
s = "The quick brown fox jumps over the lazy dog."
(s.downcase !~ /([a-z]).*\1/)
# => false
I would solve this in two steps: 1) extract the letters 2) check if there are duplicates:
letters = string.scan(/[a-z]/i) # append .downcase to ignore case
letters.length == letters.uniq.length
Here is a method that does not convert the string to an array:
def dupless?(str)
str.downcase.each_char.with_object('') { |c,s|
c =~ /[a-z]/ && s.include?(c) ? (return false) : s << c }
true
end
dupless?("Cwm fjord veg balks nth pyx quiz.") #=> true
dupless?("This is a string.") #=> false
dupless?("two") #=> true
dupless?("Two tubs") #=> false
If you want to actually keep track of the duplicate characters:
def is_unique?(string)
# Remove whitespaces
string = string.gsub(/\s+/, "")
# Build a hash counting all occurences of each characters
h = Hash.new { |hash, key| hash[key] = 0 }
string.chars.each { |c| h[c] += 1 }
# An array containing all the repetitions
res = h.keep_if {|k, c| c > 1}.keys
if res.size == 0
puts "All #{string.size} characters are used only once. This is unique"
else
puts "Not unique #{res.join(', ')} are used more than once"
end
end
is_unique?("This is a string") # Not unique i, s are used more than once
is_unique?("two") # All 3 characters are used only once. This is unique
To check if a string is unique or not, you can try out this:
string_input.downcase.gsub(/[^a-z]/, '').split("").sort.join('') == ('a' .. 'z').to_a.join('')
This will return true, if all the characters in your string are unique and if they include all the 26 characters.
def has_uniq_letters?(str)
letters = str.gsub(/[^A-Za-z]/, '').chars
letters == letters.uniq
end
If this doesn't have to be case sensitive,
def has_uniq_letters?(str)
letters = str.downcase.gsub(/[^a-z]/, '').chars
letters == letters.uniq
end
In your example, you mentioned you wanted additional information about your string (list of unique characters, etc), so this example may also be useful to you.
# s = "Cwm fjord veg balks nth pyx quiz."
s = "This is a test string."
totals = Hash.new(0)
s.downcase.each_char { |c| totals[c] += 1 if ('a'..'z').cover?(c) }
duplicates, uniques = totals.partition { |k, v| v > 1 }
duplicates, uniques = Hash[duplicates], Hash[uniques]
# duplicates = {"t"=>4, "i"=>3, "s"=>4}
# uniques = {"h"=>1, "a"=>1, "e"=>1, "r"=>1, "n"=>1, "g"=>1}

Resources