Why does capitalizing the first letter of string elements alter an array? - ruby

The following code is intended to capitalize the first letter of each word in a string, and it works:
def capitalize_words(string)
words = string.split(" ")
idx = 0
while idx < words.length
word = words[idx]
word[0] = word[0].upcase
idx += 1
end
return words.join(" ")
end
capitalize_words("this is a sentence") # => "This Is A Sentence"
capitalize_words("mike bloomfield") # => "Mike Bloomfield"
I do not understand why it works. In the while loop, I did not set any element in the words array to anything new. I understand that it might work if I added the following line before the index iteration:
words[idx] = word
I would then be altering the elements of words. However, the code works even without that line.

yet in no place in the while loop that I am using to capitalize the
first letter of each word do I actually set any of the elements in the
"words" array to anything new.
You do, actually, right here:
word = words[idx]
word[0] = word[0].upcase # This changes words[idx][0]!
The upcase method does just that: returns the upcase of a given string. For example:
'example'.upcase
# => "EXAMPLE"
'example'[0].upcase
# => "E"

The method String#[]= that you are using in:
word[0] = ...
is not variable assignment. It alters the content of the receiver string at the given index, retaining the identity of the string as an object. And since word is not a copy but is the original string taken from words, in turn, you are modifying words.

You're doing a lot of work that you don't have to:
def capitalize_words(string)
string.split.map{ |w|
[w[0].upcase, w[1..-1]].join # => "Foo", "Bar"
}.join(' ')
end
capitalize_words('foo bar')
# => "Foo Bar"
Breaking it down:
'foo'[0] # => "f"
'foo'[0].upcase # => "F"
'foo'[1..-1] # => "oo"
['F', 'oo'].join # => "Foo"

Related

How to mask all but last four characters in a string

I've been attempting a coding exercise to mask all but the last four digits or characters of any input.
I think my solution works but it seems a bit clumsy. Does anyone have ideas about how to refactor it?
Here's my code:
def mask(string)
z = string.to_s.length
if z <= 4
return string
elsif z > 4
array = []
string1 = string.to_s.chars
string1[0..((z-1)-4)].each do |s|
array << "#"
end
array << string1[(z-4)..(z-1)]
puts array.join(", ").delete(", ").inspect
end
end
positive lookahead
A positive lookahead makes it pretty easy. If any character is followed by at least 4 characters, it gets replaced :
"654321".gsub(/.(?=.{4})/,'#')
# "##4321"
Here's a description of the regex :
r = /
. # Just one character
(?= # which must be followed by
.{4} # 4 characters
) #
/x # free-spacing mode, allows comments inside regex
Note that the regex only matches one character at a time, even though it needs to check up to 5 characters for each match :
"654321".scan(r)
# => ["6", "5"]
/(.)..../ wouldn't work, because it would consume 5 characters for each iteration :
"654321".scan(/(.)..../)
# => [["6"]]
"abcdefghij".scan(/(.)..../)
# => [["a"], ["f"]]
If you want to parametrize the length of the unmasked string, you can use variable interpolation :
all_but = 4
/.(?=.{#{all_but}})/
# => /.(?=.{4})/
Code
Packing it into a method, it becomes :
def mask(string, all_but = 4, char = '#')
string.gsub(/.(?=.{#{all_but}})/, char)
end
p mask('testabcdef')
# '######cdef'
p mask('1234')
# '1234'
p mask('123')
# '123'
p mask('x')
# 'x'
You could also adapt it for sentences :
def mask(string, all_but = 4, char = '#')
string.gsub(/\w(?=\w{#{all_but}})/, char)
end
p mask('It even works for multiple words')
# "It even #orks for ####iple #ords"
Some notes about your code
string.to_s
Naming things is very important in programming, especially in dynamic languages.
string.to_s
If string is indeed a string, there shouldn't be any reason to call to_s.
If string isn't a string, you should indeed call to_s before gsub but should also rename string to a better description :
object.to_s
array.to_s
whatever.to_s
join
puts array.join(", ").delete(", ").inspect
What do you want to do exactly? You could probably just use join :
[1,2,[3,4]].join(", ").delete(", ")
# "1234"
[1,2,[3,4]].join
# "1234"
delete
Note that .delete(", ") deletes every comma and every whitespace, in any order. It doesn't only delete ", " substrings :
",a b,,, cc".delete(', ')
# "abcc"
["1,2", "3,4"].join(', ').delete(', ')
# "1234"
Ruby makes this sort of thing pretty trivial:
class String
def asteriskify(tail = 4, char = '#')
if (length <= tail)
self
else
char * (length - tail) + self[-tail, tail]
end
end
end
Then you can apply it like this:
"moo".asteriskify
# => "moo"
"testing".asteriskify
# => "###ting"
"password".asteriskify(5, '*')
# => "***sword"
Try this one
def mask(string)
string[0..-5] = '#' * (string.length - 4)
string
end
mask("12345678")
=> "####5678"
I will add my solution to this topic too :)
def mask(str)
str.match(/(.*)(.{4})/)
'#' * ($1 || '').size + ($2 || str)
end
mask('abcdef') # => "##cdef"
mask('x') # => "x"
I offer this solution mainly to remind readers that String#gsub without a block returns an enumerator.
def mask(str, nbr_unmasked, mask_char)
str.gsub(/./).with_index { |s,i| i < str.size-nbr_unmasked ? mask_char : s }
end
mask("abcdef", 4, '#')
#=> "##cdef"
mask("abcdef", 99, '#')
#=> "######"
Try using tap
def mask_string(str)
str.tap { |p| p[0...-4] = '#' * (p[0...-4].length) } if str.length > 4
str
end
mask_string('ABCDEF') # => ##CDEF
mask_string('AA') # => AA
mask_string('S') # => 'S'

why does the array elements change after block?

I am trying to solve this problem
Given a sentence containing multiple words, find the frequency of a given word in that sentence.
Construct a method named 'find_frequency' which accepts two arguments 'sentence' and 'word', both of which are String objects.
Example: The method, given 'Ruby is The best language in the World' and 'the', should return 2 (comparison should be case-insensitive).
Hint: You can use the method Array#count to count the frequency of any element in the given array.
Since the comparison should be case-insensitive. I use these code to help:
word = "the"
word_set = []
word.size.times do |i|
word[i] = word[i].upcase
word_set << word
word[i] = word[i].downcase
end
Inside the block every time after upcase method the word does change and does add to the word_set, however when the block finish the word_set just contain the the the
What is the problem?
I am still confused about that block code
The block runs 3 times with i = 0, 1, 2. Here's what happens:
# word word_set
word[0] = word[0].upcase # 'The' []
word_set << word # 'The' ['The']
word[0] = word[0].downcase # 'the' ['the']
word[1] = word[1].upcase # 'tHe' ['tHe']
word_set << word # 'tHe' ['tHe', 'tHe']
word[1] = word[1].downcase # 'the' ['the', 'the']
word[2] = word[2].upcase # 'thE' ['thE', 'thE']
word_set << word # 'thE' ['thE', 'thE', 'thE']
word[2] = word[2].downcase # 'the' ['the', 'the', 'the']
This is because you are modifying the very same string object. At the end, your array contains the same string instance three times.
You can avoid this by using dup to create a copy of your string, something like:
word = "the"
word_set = []
word.size.times do |i|
new_word = word.dup
new_word[i] = new_word[i].upcase
word_set << new_word
end
word_set #=> ["The", "tHe", "thE"]
Note that you still have to add the, THe, tHE, ThE and THE to your array.
You add the same string to the array over and over again. In the end of the loop, the array will contain the same string n times (where n ist the length of the string). So, you are changing the same string back and forth between uppercase and lowercase, but it's still just one string.
Well, word[i] = word[i].upcase is problematic, because you are setting it to upcase and downcase, the word will change over time. What you should have focused on is the Array#count method, which takes a block as a parameter.
Here is the gist of it:
def find_frequency sentence, word
sentence.split(" ").count{|w| w == word }
end
To finish it off, complete the puzzle by taking into account case sensitivity of word and sentence
You could do something like this:
class Array
def group_and_count
self.map(&:downcase).each_with_object(Hash.new(0)){|k,h|h[k] += 1}
end
end
Then when you want to find the frequency of a given word you could say:
> words = 'Here is a a a list OF OF words words WORDS'
> freq = words.split.group_and_count
> freq['a']
=> 3

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}

Outputs string if string contains vowels and is in alphabetical order

I'm working on the following exercise below:
Write a method, ovd(str) that takes a string of lowercase words and returns a string with just the words containing all their vowels (excluding "y") in alphabetical order. Vowels may be repeated ("afoot" is an ordered vowel word). The method does not return the word if it is not in alphabetical order.
Example output is:
ovd("this is a test of the vowel ordering system") #output=> "this is a test of the system"
ovd("complicated") #output=> ""
Below is code I wrote that will do the job but I am looking to see if there is a shorter more clever way to do this. My solution seems too lengthy.Thanks in advance for helping.
def ovd?(str)
u=[]
k=str.split("")
v=["a","e","i","o","u"]
w=k.each_index.select{|i| v.include? k[i]}
r={}
for i in 0..v.length-1
r[v[i]]=i+1
end
w.each do |s|
u<<r[k[s]]
end
if u.sort==u
true
else
false
end
end
def ovd(phrase)
l=[]
b=phrase.split(" ")
b.each do |d|
if ovd?(d)==true
l<<d
end
end
p l.join(" ")
end
def ovd(str)
str.split.select { |word| "aeiou".match(/#{word.gsub(/[^aeiou]/, "").chars.uniq.join(".*")}/) }.join(" ")
end
ovd("this is a test of the vowel ordering system") # => "this is a test of the system"
ovd("complicated") # => ""
ovd("alooft") # => "alooft"
ovd("this is testing words having something in them") # => "this is testing words having in them"
EDIT
As requested by the OP, explanation
String#gsub word.gsub(/[^aeiou]/, "") removes the non-vowel characters e.g
"afloot".gsub(/[^aeiou]/, "") # => "aoo"
String#chars converts the new word to an array of characters
"afloot".gsub(/[^aeiou]/, "").chars # => ["a", "o", "o"]
Array#uniq converts returns only unique elements from the array e.g
"afloot".gsub(/[^aeiou]/, "").chars.uniq # => ["a", "o"]
Array#join converts an array to a string merging it with the supplied parameter e.g
"afloot".gsub(/[^aeiou]/, "").chars.uniq.join(".*") # => "a.*o"
#{} is simply String interpolation and // converts the interpolated string into a Regular Expression
/#{"afloot".gsub(/[^aeiou]/, "").chars.uniq.join(".*")}/ # => /a.*o/
A non-regex solution:
V = %w[a e i o u] # => ["a", "e", "i", "o", "u"]
def ovd(str)
str.split.select{|w| (x = w.downcase.chars.select \
{|c| V.include?(c)}) == x.sort}.join(' ')
end
ovd("this is a test of the vowel ordering system")
# => "this is a test of the system"
ovd("") # => ""
ovd("camper") # => "camper"
ovd("Try singleton") # => "Try"
ovd("I like leisure time") # => "I"
ovd("The one and only answer is '42'") # => "The and only answer is '42'"
ovd("Oil and water don't mix") # => "and water don't mix"
Edit to add an alternative:
NV = (0..127).map(&:chr) - %w(a e i o u) # All ASCII chars except lc vowels
def ovd(str)
str.split.select{|w| (x = w.downcase.chars - NV) == x.sort}.join(' ')
end
Note x = w.downcase.chars & V does not work. While it spears out the vowels from w, and preserves their order, it removes duplicates.

Regular expression and String

With the expression below:
words = string.scan(/\b\S+\b/i)
I am trying to scan through the string with word boundaries and case insensitivity, so if I have:
string = "A ball a Ball"
then when I have this each block:
words.each { |word| result[word] += 1 }
I am anticipating something like:
{"a"=>2, "ball"=>2}
But instead what I get is:
{"A"=>1, "ball"=>1, "a"=>1, "Ball"=>1}
After this thing didnt work I tried to create a new Regexp like:
Regexp.new(Regexp.escape(string), "i")
but then I do not know how to use this or move forward from here.
The regex matches words in case-insensitive mode, but it doesn't alter matched text in any way. So you will receive text in its original form in the block. Try casting strings to lower case when counting.
string = "A ball a Ball"
words = string.scan(/\b\S+\b/i) # => ["A", "ball", "a", "Ball"]
result = Hash.new(0)
words.each { |word| result[word.downcase] += 1 }
result # => {"a"=>2, "ball"=>2}
The regexp is fine; your problem is when you increment your counter using the hash. Hash keys are case sensitive, so you must change the case when incrementing:
words.each { |word| result[word.upcase] += 1 }

Resources