I've flirted with learning web dev in the past and haven't had the time as I am a full time Business Student.
I started digging back in today and decided to take a break from the learning and practice what I've learned today by writing a simple program that allows the user to enter in their bills and will eventually calculate how much disposable income they have after their bills are paid each month.
My problem is that the program runs through perfectly, the loop is continuing/exiting when it should, but either the program is not storing the users input in the hash like I'm wanting it to or it's not displaying all the bills entered as it should. Here is my program:
# This program allows you to assign monthly payments
# to their respective bills and will automatically
# calculate how much disposable income you have
# after your bills are paid
# Prompts user to see if they have any bills to enter
puts "Do you have any bills you would like to enter, Yes or No?"
new_bill = gets.chomp.downcase
until new_bill == 'no'
# Creates a hash to store a key/value pair
# of the bill name and the respection payment amount
bills = {}
puts "Enter the bill name: "
bill_name = gets.chomp
puts "How much is this bill?"
pay_amt = gets.chomp
bills[bill_name] = pay_amt
puts "Would you like to add another bill, Yes or No?"
new_bill = gets.chomp.downcase
end
bills.each do |bill_name, pay_amt|
puts "Your #{bill_name} bill is $#{pay_amt}."
end
My questions are:
Is my hash set up properly to store the key/value pairs from the users input?
If not, how can I correct it?
I'm getting only the last bill that was entered by the user. I've tried several bills at a time but only getting the last entry.
As I stated, I'm a noob but I'm extremely ambitious to learn. I've referred to to the ruby docs on hashes to see if there is an error in my code but was able to locate a solution (still finding my way around ruby docs).
Any help is appreciated! Also, if you have any recommendations on ways I can make my code more efficient, could you point me in the direction where I can obtain the appropriate information to do so?
Thank you.
Edit:
The main question has been answered. This is a follow up question to the same program - I'm getting an error message budget_calculator.rb:35:in -': Hash can't be coerced into Float (TypeError)
from budget_calculator.rb:35:in'
From the following code (keep in mind of the program above) -
# Displays the users bills
bills_hash.each {|key,value| puts "Your #{key} bill is $#{value}."}
# Get users net income
puts "What is your net income?"
net_income = gets.chomp.to_f
#Calculates the disposable income of the user
disposable_income = net_income - bills_hash.each {|value| value}
puts disposable_income
I understand the error is appearing from this line of code:
disposable_income = net_income - bills_hash.each {|value| value}
I'm just not understanding why this is unacceptable. I'm trying to subtract all of the values in the hash (pay_amt) from the net income to derive the disposable income.
This is the part that's getting you:
bills = {}
You're resetting the hash every time the program loops. Try declaring bills at the top of the program.
As to your second question about bills_hash, it's not working because the program is attempting to subtract a hash from a float. You've got the right idea, but the way it's set up, it's not going to just subtract each key from the net_income in turn.
The return value of #each is the original hash that you were looping over. You can see this if you open IRB and type
[1,2,3].each {|n| puts n}
The block is evaluated for each element of the list, but the final return value is the original list:
irb(main):007:0> [1,2,3].each {|n| puts n}
1
2
3
=> [1, 2, 3] # FINAL RETURN VALUE
So according to the order of operations, your #each block is iterating, then returning the original bills_hash hash, and then trying to subtract that hash from net_income, which looks like this (assuming my net_income is 1000):
1000 - {rent: 200, video_games: 800}
hence the error.
There are a couple ways you could go about fixing this. One would be to sum all of the values in bills_hash as its own variable, then subtract that from the net_income:
total_expenditures = bills_hash.values.inject(&:+) # sum the values
disposable_income = net_income - total_expenditures
Using the same #inject method, this could also be done in one function call:
disposable_income = bills_hash.values.inject(net_income, :-)
# starting with net_income, subtract each value in turn
See the documentation for Enumerable#inject.
It's a very powerful and useful method to know. But make sure you go back and understand how return values work and why the original setup was raising an exception.
Related
I am new to coding, started learning Ruby arrays this week. I am a bit stuck on this problem that asks:
Create a program that asks a user to enter four different words, one at a time. Then, the computer will ask the user to choose a number between 0 and 3. The computer will then display the word corresponding to the correct number:
array = ["response_zero", "response_one", "response_two", "response_three"]
puts "Welcome! Enter 4 words, one at a time:"
times do
response = gets.chomp
end
puts "Now, guess a number between 0 and 3."
response = gets.chomp
** stuck right here on how to get the users entered number to print out the correlating array index. My professor hinted at only needing one line of code below the second response = gets.chomp, but haven't been able to figure it out for the past day.
Any help would be appreciated!
If you want to access an element in array you need to store the value of the input as Integer. So, you need to use the method to_i:
response = gets.chomp.to_i
By default, the gets method takes the user input as a String, so you need to explicitly convert it to Integer.
Now, you can easily access a particular element in the array:
puts "The word is #{array[response]}."
Where array is the name of the array and response is the number that the user entered.
Find more informations here:
class Array
to_i
I get a daily email that lists upcoming appointments, and their length. The number of appointments vary from day to day.
The emails go like this:
================
Today's Schedule
9:30 AM
3h
Brazilian Blowout
[Client #1 name]
12:30 PM
1h
Women's Cut
[Client 2 name]
6:00 PM
45m
Men's Cut
[Client #3 name]
Projected Revenue
===================
I want to create an event in a Google Calendar for each appointment, and it seems like zapier MIGHT be able to do this, but all the help resources I can find are very general in nature.
Is this do-able on Zapier? If so, any nudges in the right direction would be awesome.
Any thoughts greatly appreciated.
I had some time to kill and enjoy the odd challenge. So I have put together a solution that should do what you are looking for. I will break it down by steps.
TEMPLATE
Zapier Trigger - Step 1
Type: Trigger
Module: Gmail
Criteria: User Dependent
Comments: For the trigger zap you will want to use a Gmail specific trigger, something to the effect of "execute trigger on emails titled 'xyz'", or "emails labeled 'xyz'" if you setup a filter in your inbox.
Input screenshot:
Output Screenshot:
Zapier Action - Step 2
Type: Action
Module: Code (Python 3)
Comments: The Code offered by Zapier executes whatever (properly written) code you place in its container. It is especially handy as it allows you to incorporate data from previous steps in it through the use of a dictionary variable titled 'input_data'. Zapier offers the Code module in two languages: Javascript and Python. As I am most familiar with Python my solution for this step was written in Python. I will append the code to the end of this answer. Using the data held in the body of the email (retrieved in step 1) we can execute some string manipulations and datetime conversions to break apart the email into its component parts and pass those on to the following Action Step: Create Calendar Event.
Input Screenshot:
Output Screenshot:
Zapier Action - Step 3
Type: Action
Module: Google Calendar - Create Event
Comments: Using the data outputted from the previous code step we can fill out the required fields for creating a new appointment.
Input Screenshot:
Output Screenshot:
PYTHON CODE
from datetime import timedelta, date, datetime
'''
Goal: Extract individual appointment details from variable length email
Steps:
Remove all extraneous and new line characters.
Isolate each individual appointment and group its relevant details.
Derive appointment start and end times using appointment time and duration.
Return all appointments in a list.
'''
def format_appt_times(appt_dict):
appt_start_str = appt_dict.get("appt_start")
appt_dur_str = appt_dict.get("appt_length")
# isolate hour and minutes from appointment time
appt_s_hour = int(appt_start_str[:appt_start_str.find(":")])
if ("pm" in appt_start_str.lower()):
appt_s_hour = 12 if appt_s_hour + 12 >= 24 else appt_s_hour + 12
appt_s_min = int(appt_start_str[appt_start_str.find(":") + 1 :
appt_start_str.find(":") + 3])
# isolate hour and minutes from duration time
appt_d_hour = 0
appt_d_min = 0
if ("h" in appt_dur_str):
appt_d_hour = int(appt_dur_str[:appt_dur_str.find("h")])
if ("m" in appt_dur_str):
appt_d_min = int(appt_dur_str[appt_dur_str.find("m") - 2 : appt_dur_str.find("m")])
# NOTE: adjust timedelta hours depending on your relation to UTC
# create datetime objects for appointment start and end times
time_zone = timedelta(hours=0)
tdy = date.today() - time_zone
duration = timedelta(hours=appt_d_hour, minutes=appt_d_min)
appt_start_dto = datetime(year=tdy.year,
month=tdy.month,
day=tdy.day,
hour=appt_s_hour,
minute=appt_s_min)
appt_end_dto = appt_start_dto + duration
# return properly formatted datetime as string for use in next step.
return (appt_start_dto.strftime("%Y-%m-%dT%H:%M"),
appt_end_dto.strftime("%Y-%m-%dT%H:%M"))
def partition_list(target, part_size):
for data in range(0, len(target), part_size):
yield target[data : data + part_size]
def main():
# Remove all extraneous and new line characters.
email_body = input_data.get("email_body")
head,delin,*email_body,delin,foot = [text for text in email_body.splitlines() if text != ""]
appointment_list = []
# Isolate each individual appointment and group its relevant details.
for text in partition_list(email_body, 4):
template = {
"appt_start" : text[0],
"appt_end" : None,
"appt_length" : text[1],
"appt_title" : text[2],
"appt_client" : text[3]
}
appointment_list.append(template)
for appt in appointment_list:
appt["appt_start"], appt["appt_end"] = format_appt_times(appt)
return appointment_list
return main()
I am not sure of your familiarity with Python, or programming more generally, but the comments in the code explain what each section is doing. If you have any specific questions regarding aspects of the code let me know. Assuming your email template does not change this setup should work exactly as needed. Let me know if anything is unclear.
UPDATE
I thought it best to address your question in the original answer should anyone else have similar questions.
explaining how this code is removing the extra characters:
There is actually a fair bit going on in the first line, so I will do my best to break it down, and provide resources where necessary.
The code in question:
head,delin,*email_body,delin,foot = [text for text in email_body.splitlines() if text != ""]
First step here was to break the text into manageable chunks. I did so with the line email_body.splitlines() which, by default, breaks strings into a list at each newline character found (you can specify your own delimiter).
If we were to inspect the list at this moment its contents would be something of the following:
["================", "", "Today's Schedule", "", "9:30 AM", "", "3h", ..., "[Client #3 name]", "", "Projected Revenue", "", "==================="]
You will notice there is a fair amount of information in there that we really don't want.
First lets look at the "" elements. These are left over as a result of the blank lines between each line of text, which even though they are blank do still have newline characters at the end of them. There a number of ways you could address this within python. We could simply write a for-loop to go through and copy all elements that are not "" to a new list.
To me this felt like additional work, and besides, Python offers list comprehension for just such a scenario. I won't go too deep into list comprehension as there is a lot that can be said about it, and in more insightful ways than I could muster, but it essentially allows you to provide logic against a set of 'data' to form a list. In this case, I specifically wanted to filter out the "" elements returned from the call to splitlines().
And so you will see I address this with the following line
[text for text in email_body.splitlines() if text != ""]
With that we have a list as above less the "" elements. Now we must turn our attention towards the more 'dynamic' garbage strings. Again there are a number of ways to do this. A, not particularly flexible, option could be to simply store the strings we want to remove in variables something to the effect of:
garb_1 = "==================="
garb_2 = "Projected Revenue"
garb_3 = ...
and once again filter the list with yet another for-loop. I instead chose to leverage Python's list unpacking idiom. Which allows us to 'unpack' list objects (and I believe tuples) into variables. As an example:
one, two, three = ["a", "b", "c"]
I'm sure you can guess what is happening above, as long as we provide the same number of variables as are in the list we can 'unpack' it in this fashion. But wait! In our case we don't know how long the list is going to be as it is entirely dependent on the number of appointments you have for any given day. Well this is where star unpacking enters to elevate the functionality. Using my code as the example:
head,delin,*email_body,delin,foot = [text for text in email_body.splitlines() if text != ""]
The *, in plain-English, is saying "I don't know how many elements to expect just give me all of them in a list". As we know that there will always be two lines of garbage at the beginning and end of the email we can assign them to throw away variables and capture everything in between using our variable length *email_body container.
With all of this complete we now have a list with only the data we are looking to capture. If, as you say, there are additional lines of garbage before or after the email_body, you can simply add additional throw away variables to account for them.
Once again feel free to ask any follow up questions.
Michael
Resources
List Comprehension
Star Unpacking
My user can have many questions, however the questions are asked in different frequencies. Like weekly, biweekly, monthly, quarterly. Now I store the frequency of a Question in a QuestionFrequency model. That accepts frequency:string and begins:string.
The values accepted for frequency are:
weekly
biweekly
monthly
quarterly
now I use this together with the begins to understand the setting. So begins accepts:
if its biweekly I note down the week number if wants it to start
(thus I can check if that week number is odd or even)
if it's monthly it saves "end" or "beginning" thus I can check if its beginning of month with rails.
quarterly it saves "end" or "beginning"
Thus I can call
question.question_frequency.frequency
f.ex to get one of the 4 accepted values. Now what I'm trying to do is create a grouped list of all questions that might be available to the User in this week.
I have a method in my user model called all_questions, which job it is to get all questions that is relevant to a user "this" week.
# Collection of Users weekly questions
def all_questions
questions
end
now how can I filter "questions" to get things like
if biweekly.odd? and Time.zone.now.strftime("%V").odd?
then add that question whilst if one is odd || even then we don't want that question this week.
I would handle it differently.
Remove the QuestionFrequency model.
Add a frequency column to Question as an integer and use Rails' Enum method to define the frequency names.
Add a valid_at date/datetime column to the Question model and have it set to the next valid date (either 1 week from now, 2 weeks, 1 month, etc.) depending on the frequency.
Now, once a question is shown to the user (or when it's answered), have the valid_at column update for the question according to its frequency:
##question.rb example
enum frequency: [:weekly, :biweekly, :monthly]
before_save :update_valid_at
def update_valid_at
if weekly?
self[:valid_at] = 1.week.from_now
elsif biweekly?
self[:valid_at] = 2.weeks.from_now
elsif monthly?
self[:valid_at] = 1.month.from_now
end
end
This way, you can change your all_questions to:
def all_questions
questions.where('valid_at < ?', Date.today)
end
Now i am making a web application (Online word learning) that allow user to choose the correct meaning of the word. When they click start, it will select randomly one word from the database and show to the user. After the user choose the answer, it will go to the next question.
Please see the image below:
If i use, Word.order("rand()").limit(1), i wonder can the word will be repeated with the last selected word?
With the app as in the image above, any better ideas to solve this problem?
I would add the following scopes to the model (depends on the database you are using):
# in app/models/word.rb
# 'RANDOM' works with postgresql and sqlite, whereas mysql uses 'RAND'
scope :random, -> { order('RAND()') }
scope :without, ->(ids) { where.not(id: ids) }
With that scopes you can write the following query in your controller:
#word = Word.random.without(params[:last_ids]).limit(1)
When you want to load new random elements in the view, just add the ids of the current words to the request. This ensures that this ids (params[:last_ids]) are not randomly choosen.
Long story short, in order not to repeat yourself, you have to store those words somewhere. Either the ones that are yet to be shown, or the ones that have been already displayed. And If I were you I would go one of the following routes:
Fetch all the words before starting the quiz and randomize them. This could be something like:
session[:words] = Word.order("RAND()").select(:id).take(10)
Or even better by defining a scope for your random words:
class Word < ActiveRecord::Base
# ...
scope :random_quiz, -> { order("RAND()").take(10).pluck(:id) }
# ...
end
# ... in the controller when the quiz is getting started:
session[:words] = Word.random_quiz
# ... in the controller when you want to show the word:
new_word = Word.find(sessions[:words].pop)
As ORDER BY RAND() is a very expensive operation, this might make sense. And then you just pop the word ID's one by one by using session[:words].pop and present the questions.
This way it will guarantee that you won't repeat the words in the quiz and give you pretty optimal performance.
Fetch words one by one as you're progressing with giving out the questions and save the ones you've already asked about.
class Word < ActiveRecord::Base
# ...
def self.random_word(exclusions)
eligible = where('id NOT IN (?)', exclusions)
eligible.offset(rand(0..eligible.count)).take!(1)
end
# ..
end
# ... in the controller when you need a new word:
session[:words_shown] ||= [ ]
new_word = Word.random_word(session[:words_shown])
# mark the word as shown:
session[:words_shown].push(new_word.id)
You might have noticed the weird way of getting a random record in the second example. It turns out to be more efficient as it generates the following query:
SELECT * FROM words OFFSET _random_number_ LIMIT 1
Instead of:
SELECT * FROM words ORDER BY RAND() LIMIT 1
The first one is just an ordinary select, while the second one requires unindexed sorting by RAND() of the entire table before giving you that random result. Turns out to be the former is almost tenfold faster than the latter.
Hope that makes sense!
This is my first attempt at learning Ruby, and I'm almost there.. this is working except for one piece.. in the function pc_draw the script asks the user if they have a card. If they do then they should be removed from both the "computer's" hand as well as the users hand.. But some reason its not.. Any idea why?
Love to hear some feedback on the code in general as well, any tips or ways to write more concisely or efficiently??
Thanks! and here is the code (edited to just include the relevant code..
def pc_draw # this is the PC playing, id dups, if none then draw, if some then delete and draw..
#first I have to identify the duplicates..
dup = #pc_cards.detect {|e| #pc_cards.rindex(e) != #pc_cards.index(e) }
if dup == nil # if no dups exist then PC has to ask for a card..
#ask_card = rand(#pc_cards.length) # assigns a random number limited to the length of the array
puts "#{#name} do you have a #{#pc_cards[#ask_card]}? Yes or No? " # ask for a random card in the array..
#user_answer = $stdin.gets.chomp
pc_gofish(#user_answer, #ask_card)
else #if there are dups then take them out of the array.
#pc_cards.delete(dup)
#ask_card = rand(#pc_cards.length) # assigns a random number limited to the length of the array
puts "#{#name} do you have a #{#pc_cards[#ask_card]}? Yes or No? " # ask for a random card in the array..
#user_answer = $stdin.gets.chomp
pc_gofish(#user_answer, #ask_card)
end
end
def pc_gofish(take, card_delete) #this will deal with the PC asking the user for cards
if take == "yes" # if the user enters in that their is a match we delete it from pc & user array
#pc_cards.delete(card_delete)
#kates_cards.delete(card_delete)
puts "The computer has #{#pc_cards.length} cards left!" #lets user know how many cards the PC has left
if #pc_cards.length == 0 # if the array is empty then the pc won!
puts "The computer won!! Sorry!"
else #pc_cards.length != 0 # if the array isn't empty, turn reverts back to user.
draw
end
else #this is when there is no match.. so PC has to take a card..
pc_random = rand(#cards.length) # this assigns a random number constrained to the length of the cards remaining
#pc_cards << pc_random # this inserts that random card into the users ask
#cards.delete(pc_random) # this takes the card from the deck of cards..
puts "The computer has #{#pc_cards.length} cards left!" #lets user know how many cards the PC has left
draw
end
end
It looks to me like your detect block is only going to return the first value of the first duplicated item; if you have more than one duplicate, the other duplicates won't be removed.
Eg:
a = [1,2,2,3,4,5,5,5,6]
dups = a.detect{|e| a.rindex(e) != a.index(e)} # => returns 2, but not 5
I don't know how you've modeled your cards, but you could possibly just uniq the array if you're concerned that duplicates exist:
#pc_cards.uniq! # deletes non-uniq elements from the array.
Update: I re-read your question, I see you're asking about the computers cards, which I assume are kept in #cards? Ultimately, I need to agree with the comments above: you have too much code here, and you aren't clearly saying which part is failing.
I would look into either debugger or pry to inspect your code as it runs, or simply put some puts statements into your code so you can see exactly what is really happening at each point.
Try using delete! Instead of delete.