class Book
# write your code here
attr_accessor :title
def title= (title)
#title = title.split()
#title = #title.map {
|x|
index = #title.index(x)
if x == 'and' or x == 'in' or x == 'of' or x == 'the' or x == 'a' or x == 'an' and index != 0
x = x
else
x.capitalize
end
}
#title = #title.join(" ")
return #title
end
end
This is an exercise from the Project Ruby on the Odin Project. It's about capitalizing the titles of a book bound to some certain conditions life if the word is a preposition or an article or a conjunction then dont capitalize it unless it occurs in the beginning of the title then capitalize it. I have written the code for it but it isn't working as you can see :
index = #title.index(x)
if x == 'and' or x == 'in' or x == 'of' or x == 'the' or x == 'a' or x == 'an' and index != 0
x = x
else
x.capitalize
end
But again it doesn't work
expected: "The Man in the Iron Mask"
got: "The Man in The Iron Mask"
The second The gets capitalized too when I have said in the if statement that if it isn't equal to the first word then don't capitalize it but it still capitalizes it.
Because index(x) always returns the first match.
I would rewrite it like this:
class Book
attr_accessor :title
DO_NOT_CAPITALIZE = %w[and in of the a an]
def title=(title)
words = title.split.map do |word|
# capitalize all word that are not excluded
DO_NOT_CAPITALIZE.include?(word) ? word : word.capitalize
end
# always capitalize the first word
#title = words.join(' ').capitalize
end
end
The problem with your code has been identified. One way to obtain the desired result is to use String#gsub with a simple regular expression.
LITTLE_WORDS = %w| a an and in of the |
#=> ["a", "an", "and", "in", "of", "the"]
LWR = /\b#{LITTLE_WORDS.join('|')}\b/i
#=> /\ba|an|and|in|of|the\b/i
def normalize(str)
str.gsub(/\S+/) do |word|
if Regexp.last_match.begin(0).zero?
word.capitalize
else
word.match?(LWR) ? word.downcase : word.capitalize
end
end
end
normalize("tHe cat and A hAt")
#=> "The Cat and a Hat"
See Regexp::last_match (same as the value of the global variable $~) and MatchData#begin. Notice that this preserves spacing in the string.
In the publishing industry such articles, prepositions and conjunctions are often referred to as "little words", and written "a", "an", "and", "in", "of", "the".
I'm working through The Odin Projects Ruby basics and completely stuck on 05_book_titles.
The title needs to be capitalized, including the 1st word but not including "small words" (ie "to", "the", etc) UNLESS it's the 1st word.
I can't get the code to do anything besides capitalize everything. Am I misusing map method? How can I get it to include the no_cap words in the returned title without capitalizing?
The Ruby file:
class Book
def title
#title
end
def title=(title)
no_cap = ["if", "or", "in", "a", "and", "the", "of", "to"]
p new_title = #title.split(" ")
p new_new_title = new_title.map{|i| i.capitalize if !no_cap.include? i}
.join(" ")
end
end
Some of the Spec file:
require 'book'
describe Book do
before do
#book = Book.new
end
describe 'title' do
it 'should capitalize the first letter' do
#book.title = "inferno"
expect(#book.title).to eq("Inferno")
end
it 'should capitalize every word' do
#book.title = "stuart little"
expect(#book.title).to eq("Stuart Little")
end
describe 'should capitalize every word except...' do
describe 'articles' do
specify 'the' do
#book.title = "alexander the great"
expect(#book.title).to eq("Alexander the Great")
end
specify 'a' do
#book.title = "to kill a mockingbird"
expect(#book.title).to eq("To Kill a Mockingbird")
end
specify 'an' do
#book.title = "to eat an apple a day"
expect(#book.title).to eq("To Eat an Apple a Day")
end
end
specify 'conjunctions' do
#book.title = "war and peace"
expect(#book.title).to eq("War and Peace")
end
end
end
end
Result:
Book
title
should capitalize the first letter (FAILED - 1)
Failures:
1) Book title should capitalize the first letter
Failure/Error: #book.title = "inferno"
NoMethodError:
undefined method `split' for nil:NilClass
# ./05_book_titles/book.rb:8:in `title='
# ./05_book_titles/book_titles_spec.rb:25:in `block (3 levels) in <top (required)>'
Finished in 0.0015 seconds (files took 0.28653 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./05_book_titles/book_titles_spec.rb:24 # Book title should capitalize the first letter
You are using #title before it's assigned in
new_title = #title.split(" ")
It should be changed to title.
You don't assign the calculated title to #title at the end of the title= method.
You also need to add 'an' to no_cap in order to pass the spec using "to eat an apple a day" as title.
And take care of the first word:
class Book
def title
#title
end
def title=(title)
no_cap = ["if", "or", "in", "a", "and", 'an', "the", "of", "to"]
new_title = title.split(' ').each_with_index.map do |x, i|
unless i != 0 && no_cap.include?(x)
x.capitalize
else
x
end
end
#title = new_title.join(' ')
end
end
small_words = ["if", "or", "in", "a", "and", "the", "of", "to"]
str = "tO be Or Not to be."
str.gsub(/\p{Alpha}+/) { |s| Regexp.last_match.begin(0) > 0 &&
small_words.include?(s.downcase) ? s.downcase : s.capitalize }
#=> "To Be or Not to Be."
I have requirements with my Title class as below.
Take an all-lower-case string like "the united states" and make the initial letter in each word capitalized ("The United States").
Take a camel case string like "ThE UnIted STatEs" and make it "The United States".
The following code satisfies them:
class Title
attr_accessor :string
def initialize(string)
#string = string
end
def fix
string2 = string.split(" ").map{ |string| string.capitalize }.join(" ")
end
end
I added another condition:
If the string is "the", "The", "of", "Of", it does not capitalize it.
The attempt to modify fix with map logic as below did not work:
class Title
def fix
string2 = string.split(" ").map{ |string| string.capitalize }.join(" ")
string2.split(" ").map{ |string| (string.include?("of","Of","the","The") ? string.downcase : string.capitalize) }.join(" ")
end
end
#=> Error: wrong number of arguments (2 for 1)
Is there another way I can implement this logic? I'm not sure why this isn't working for me. Can anyone offer any assistance/guidance?
String#include only takes one argument, that's where the ArgumentError is coming from. Instead you could do something like:
[8] pry(main)> prepositions = ["of", "Of", "the", "The"]
=> ["of", "Of", "the", "The"]
[9] pry(main)> string2.split(" ").map{ |string| prepositions.include?(string) ? string.downcase : string.capitalize }.join(" ")
=> "of Thy Self In the Capital"
I prefer the above, it allows you to easily keep a list of words that are outside the normal capitalization method. It's easy to read, easy to add to etc. That said, you can use case insensitive regex with match as well:
string2.split(" ").map{ |string| string.match(/(the)|(of)/i) ? string.downcase : string.capitalize }.join(" ")
Use gsub
You don't need to convert the string to an array of words, map the words, then join. Instead, just use the form of String#gsub that takes a block.
Little Words
You said you do not want to capitalize certain words. Editors often refer to such words as "little words". Let's define a few:
LITTLE_WORDS = %w{ the of for a an or and }
#=> ["the", "of", "for", "a", "an", "or", "and"]
Code
I assume that all little words encountered are be downcased, and all other words are to be downcased and capitalized. We can do that thus:
def fix(str)
str.gsub(/\w+/) do |w|
if LITTLE_WORDS.include?(w.downcase)
w.downcase
else
w.capitalize
end
end
end
Examples
Let's try it:
fix("days of wine aNd roses") #=> "Days of Wine and Roses"
fix("of mice and meN") #=> "of Mice and Men"
Hmmm. A bit of a problem with the second example. Presumably, we should capitalize the first word regardless of whether it's a little word. There are various ways to do that.
#1 Capitalize the first word after modifying all words
def fix(str)
str.gsub(/\w+/) do |w|
if LITTLE_WORDS.include?(w.downcase)
w.downcase
else
w.capitalize
end
end.sub(/^(\w+)/) { |s| s.capitalize }
end
fix("of mice and men")
#=> "Of Mice and Men"
Notice that I've introduced a capture group in the regex. Alternatively, you could change the penultimate line to:
end.sub(/^(\w+)/) { $1.capitalize }
#2 Set a flag
def fix(str)
first_word = true
str.gsub(/\w+/) do |w|
if LITTLE_WORDS.include?(w.downcase) && !first_word
w.downcase
else
first_word = false
w.capitalize
end
end
end
fix("of mice and men")
#=> "Of Mice and Men"
#3 Use an index
def fix(str)
str.gsub(/\w+/).with_index do |w,i|
if LITTLE_WORDS.include?(w.downcase) && i > 0
w.downcase
else
w.capitalize
end
end
end
fix("of mice and men")
#=> "Of Mice and Men"
#4 Modify the regex
def fix(str)
str.gsub(/(^\w+)|\w+/) do |w|
if $1.nil? && LITTLE_WORDS.include?(w.downcase)
w.downcase
else
w.capitalize
end
end
end
fix("of mice and men")
#=> "Of Mice and Men"
More problems
Now we need just fix:
fix("I bought an iPhone and a TV")
#=> "I Bought an Iphone and a Tv"
I have this exercise:
Write a Title class which is initialized with a string.
It has one method -- fix -- which should return a title-cased version of the string:
Title.new("a title of a book").fix =
A Title of a Book
You'll need to use conditional logic - if and else statements - to make this work.
Make sure you read the test specification carefully so you understand the conditional logic to be implemented.
Some methods you'll want to use:
String#downcase
String#capitalize
Array#include?
Also, here is the Rspec, I should have included that:
describe "Title" do
describe "fix" do
it "capitalizes the first letter of each word" do
expect( Title.new("the great gatsby").fix ).to eq("The Great Gatsby")
end
it "works for words with mixed cases" do
expect( Title.new("liTTle reD Riding hOOD").fix ).to eq("Little Red Riding Hood")
end
it "downcases articles" do
expect( Title.new("The lord of the rings").fix ).to eq("The Lord of the Rings")
expect( Title.new("The sword And The stone").fix ).to eq("The Sword and the Stone")
expect( Title.new("the portrait of a lady").fix ).to eq("The Portrait of a Lady")
end
it "works for strings with all uppercase characters" do
expect( Title.new("THE SWORD AND THE STONE").fix ).to eq("The Sword and the Stone")
end
end
end
Thank you #simone, I incorporated your suggestions:
class Title
attr_accessor :string
def initialize(string)
#string = string
end
IGNORE = %w(the of a and)
def fix
s = string.split(' ')
s.map do |word|
words = word.downcase
if IGNORE.include?(word)
words
else
words.capitalize
end
end
s.join(' ')
end
end
Although I'm still running into errors when running the code:
expected: "The Great Gatsby"
got: "the great gatsby"
(compared using ==)
exercise_spec.rb:6:in `block (3 levels) in <top (required)>'
From my beginner's perspective, I cannot see what I'm doing wrong?
Final edit: I just wanted to say thanks for all the effort every one put in in assisting me earlier. I'll show the final working code I was able to produce:
class Title
attr_accessor :string
def initialize(string)
#string = string
end
def fix
word_list = %w{a of and the}
a = string.downcase.split(' ')
b = []
a.each_with_index do |word, index|
if index == 0 || !word_list.include?(word)
b << word.capitalize
else
b << word
end
end
b.join(' ')
end
end
Here's a possible solution.
class Title
attr_accessor :string
IGNORES = %w( the of a and )
def initialize(string)
#string = string
end
def fix
tokens = string.split(' ')
tokens.map do |token|
token = token.downcase
if IGNORES.include?(token)
token
else
token.capitalize
end
end.join(" ")
end
end
Title.new("a title of a book").fix
Your starting point was good. Here's a few improvements:
The comparison is always lower-case. This will simplify the if-condition
The list of ignored items is into an array. This will simplify the if-condition because you don't need an if for each ignored string (they could be hundreds)
I use a map to replace the tokens. It's a common Ruby pattern to use blocks with enumerations to loop over items
There are two ways you can approach this problem:
break the string into words, possibly modify each word and join the words back together; or
use a regular expression.
I will say something about the latter, but I believe your exercise concerns the former--which is the approach you've taken--so I will concentrate on that.
Split string into words
You use String#split(' ') to split the string into words:
str = "a title of a\t book"
a = str.split(' ')
#=> ["a", "title", "of", "a", "book"]
That's fine, even when there's extra whitespace, but one normally writes that:
str.split
#=> ["a", "title", "of", "a", "book"]
Both ways are the same as
str.split(/\s+/)
#=> ["a", "title", "of", "a", "book"]
Notice that I've used the variable a to signify that an array is return. Some may feel that is not sufficiently descriptive, but I believe it's better than s, which is a little confusing. :-)
Create enumerators
Next you send the method Enumerable#each_with_index to create an enumerator:
enum0 = a.each_with_index
# => #<Enumerator: ["a", "title", "of", "a", "book"]:each_with_index>
To see the contents of the enumerator, convert enum0 to an array:
enum0.to_a
#=> [["a", 0], ["title", 1], ["of", 2], ["a", 3], ["book", 4]]
You've used each_with_index because the first word--the one with index 0-- is to be treated differently than the others. That's fine.
So far, so good, but at this point you need to use Enumerable#map to convert each element of enum0 to an appropriate value. For example, the first value, ["a", 0] is to be converted to "A", the next is to be converted to "Title" and the third to "of".
Therefore, you need to send the method Enumerable#map to enum0:
enum1 = enum.map
#=> #<Enumerator: #<Enumerator: ["a", "title", "of", "a",
"book"]:each_with_index>:map>
enum1.to_a
#=> [["a", 0], ["title", 1], ["of", 2], ["a", 3], ["book", 4]]
As you see, this creates a new enumerator, which could think of as a "compound" enumerator.
The elements of enum1 will be passed into the block by Array#each.
Invoke the enumerator and join
You want to a capitalize the first word and all other words other than those that begin with an article. We therefore must define some articles:
articles = %w{a of it} # and more
#=> ["a", "of", "it"]
b = enum1.each do |w,i|
case i
when 0 then w.capitalize
else articles.include?(w) ? w.downcase : w.capitalize
end
end
#=> ["A", "Title", "of", "a", "Book"]
and lastly we join the array with one space between each word:
b.join(' ')
=> "A Title of a Book"
Review details of calculation
Let's go back to the calculation of b. The first element of enum1 is passed into the block and assigned to the block variables:
w, i = ["a", 0] #=> ["a", 0]
w #=> "a"
i #=> 0
so we execute:
case 0
when 0 then "a".capitalize
else articles.include?("a") ? "a".downcase : "a".capitalize
end
which returns "a".capitalize => "A". Similarly, when the next element of enum1 is passed to the block:
w, i = ["title", 1] #=> ["title", 1]
w #=> "title"
i #=> 1
case 1
when 0 then "title".capitalize
else articles.include?("title") ? "title".downcase : "title".capitalize
end
which returns "Title" since articles.include?("title") => false. Next:
w, i = ["of", 2] #=> ["of", 2]
w #=> "of"
i #=> 2
case 2
when 0 then "of".capitalize
else articles.include?("of") ? "of".downcase : "of".capitalize
end
which returns "of" since articles.include?("of") => true.
Chaining operations
Putting this together, we have:
str.split.each_with_index.map do |w,i|
case i
when 0 then w.capitalize
else articles.include?(w) ? w.downcase : w.capitalize
end
end
#=> ["A", "Title", "of", "a", "Book"]
Alternative calculation
Another way to do this, without using each_with_index, is like this:
first_word, *remaining_words = str.split
first_word
#=> "a"
remaining_words
#=> ["title", "of", "a", "book"]
"#{first_word.capitalize} #{ remaining_words.map { |w|
articles.include?(w) ? w.downcase : w.capitalize }.join(' ') }"
#=> "A Title of a Book"
Using a regular expression
str = "a title of a book"
str.gsub(/(^\w+)|(\w+)/) do
$1 ? $1.capitalize :
articles.include?($2) ? $2 : $2.capitalize
end
#=> "A Title of a Book"
The regular expression "captures" [(...)] a word at the beginning of the string [(^\w+)] or [|] a word that is not necessarily at the beginning of string [(\w+)]. The contents of the two capture groups are assigned to the global variables $1 and $2, respectively.
Therefore, stepping through the words of the string, the first word, "a", is captured by capture group #1, so (\w+) is not evaluated. Each subsequent word is not captured by capture group #1 (so $1 => nil), but is captured by capture group #2. Hence, if $1 is not nil, we capitalize the (first) word (of the sentence); else we capitalize $2 if the word is not an article and leave it unchanged if it is an article.
def fix
string.downcase.split(/(\s)/).map.with_index{ |x,i|
( i==0 || x.match(/^(?:a|is|of|the|and)$/).nil? ) ? x.capitalize : x
}.join
end
Meets all conditions:
a, is, of, the, and all lowercase
capitalizes all other words
all first words are capitalized
Explanation
string.downcase calls one operation to make the string you're working with all lower case
.split(/(\s)/) takes the lower case string and splits it on white-space (space, tab, newline, etc) into an array, making each word an element of the array; surrounding the \s (the delimiter) in the parentheses also retains it in the array that's returned, so we don't lose that white-space character when rejoining
.map.with_index{ |x,i| iterates over that returned array, where x is the value and i is the index number; each iteration returns an element of a new array; when the loop is complete you will have a new array
( i==0 || x.match(/^(?:a|is|of|the|and)$/).nil? ) if it's the first element in the array (index of 0), or the word matches a,is,of,the, or and -- that is, the match is not nil -- then x.capitalize (capitalize the word), otherwise (it did match the ignore words) so just return the word/value, x
.join take our new array and combine all the words into one string again
Additional
Ordinarily, what is inside parentheses in regex is considered a capture group, meaning that if the pattern inside is matched, a special variable will retain the value after the regex operations have finished. In some cases, such as the \s we wanted to capture that value, because we reuse it, in other cases like our ignore words, we need to match, but do not need to capture them. To avoid capturing a match you can pace ?: at the beginning of the capture group to tell the regex engine not to retain the value. There are many benefits of this that fall outside the scope of this answer.
Here is another possible solution to the problem
class Title
attr_accessor :str
def initialize(str)
#str = str
end
def fix
s = str.downcase.split(" ") #convert all the strings to downcase and it will be stored in an array
words_cap = []
ignore = %w( of a and the ) # List of words to be ignored
s.each do |item|
if ignore.include?(item) # check whether word in an array is one of the words in ignore list.If it is yes, don't capitalize.
words_cap << item
else
words_cap << item.capitalize
end
end
sentence = words_cap.join(" ") # convert an array of strings to sentence
new_sentence =sentence.slice(0,1).capitalize + sentence.slice(1..-1) #Capitalize first word of the sentence. Incase it is not capitalized while checking the ignore list.
end
end
So i need to turn an array of strings into a sentence, capitalize the first word and add a period at the end. I have looked everywhere and found bits and pieces but nothing as specific as my problem.
What i tried so far:
array1 = ["this", "is", "my", "first", "post"]
def sentence_maker (array)
array.join(' ')
end
It makes a sentence but i can't figure out how to make the first word capitalized while keeping the others in lower case and add a "." at the end of the sentence. Any help would be appreciated.
You could do as below :
array1 = ["this", "is", "my", "first", "post"]
def sentence_maker (array)
array.join(' ').capitalize << "."
end
sentence_maker(array1)
# => "This is my first post."
How I would do it:
array1 = ["this", "is", "my", "first", "post"]
def sentence_maker(array)
string = array.join(' ')
string.capitalize!
string << '.'
end
puts sentence_maker(array1)
#=> "This is my first post."
See: http://www.ruby-doc.org/core-1.9.3/String.html
The simple way to do this would be to use the capitalize method, but note that “case conversion is effective only in ASCII region”:
"école".capitalize
# => "école"
If this is likely to be an issue you should look into using something like the Unicode Utils gem:
require 'unicode_utils'
UnicodeUtils.titlecase("école")
# => "École"
So your complete method might look something like:
def sentence_maker (array)
array[0] = UnicodeUtils.titlecase(array[0])
array.join(' ') << '.'
end
(This is a bit different from the other answers because titlecase changes the first letter of each word in the string, which we don’t want in this case. Also note this modifies array which you might not want, so you”d have to structure the code differently if that were the case.)