Outputting hash to text file - ruby

I am having trouble outputting the contents of my hash to a file. The program is one that manages a list of student records, including their StudentID, first name, last name, Major, and catalog year. Once the user is finished adding records, it is then added to the hash.
Everything in the program works perfectly, except when I try running the quit_program function, it doesn't save the contents in the file. Additionally, i am not getting any errors, any ideas?
could it potentially not be working because it is having trouble with converting the text in my hash, which is alphanumeric, into the text file?
def quit_program()
puts "Save Changes? y/n"
#changes = gets().chomp
if #changes=="y"
#fh=File.open(#file_name, 'w')
#this_string=""
#sDB.each do |key, store_account_data| #line 50
puts "#{key}: #{store_account_data.join(',')}"
end
end
#fh.puts(#this_string)
#fh.close()
end

You're not writing anything to the file. The string #this_string is empty. You should do
#sDB.each do |key, store_account_data|
#fh.puts "#{key}: #{store_account_data.join(',')}"
end

it doesn't save the contents in the file.
The following is NOT how you write to a file:
puts "#{key}: #{store_account_data.join(',')}"
That is how you write to your terminal/console window.
And this code:
#this_string=""
#fh.puts(#this_string)
writes a blank string to the file.
Here is how you write to a file:
class Student
def initialize(sDB, filename)
#sDB = sDB
#filename = filename
end
def save_changes()
puts "Save Changes? y/n"
user_answer = gets().chomp
if user_answer == "y"
File.open(#file_name, 'w') do |f|
#sDB.each do |key, store_account_data| #line 50
f.puts "#{key}: #{store_account_data.join(',')}"
end
end
end
end
could it potentially not be working because it is having trouble with
converting the text in my hash, which is alphanumeric, into the text
file?
No. Here is a concrete example you can try:
data = {
"John" => ['a', 123, 'b', 456],
"Sally" => ['c', 789, 'b', 0]
}
File.open('data.txt', 'w') do |f|
data.each do |name, data|
f.puts "#{name}: #{data.join(',')}"
end
end
$ ruby myprog.rb
$ cat data.txt
John: a,123,b,456
Sally: c,789,b,0
Also, ruby indenting is 2 spaces--not 0 spaces or 3 spaces, or anything else.

The answer is given in the error message: undefined local variable or method 'sDB'. (Which you have since removed from your question making the edited version next to impossible to answer.) Where and when is sDB defined in your program? You are evidently attempting to quit before initializing it.
In any case it is not a good thing to be accessing instance variables directly inside other methods. You should use accessor (getter and setter) methods instead. That would have probably prevented this situation from biting you in the first place.
def sdb
#sDB ||= Hash.new
end
def sdb=( key, value )
sdb
#sDB[ key ] = value
end
. . .
You are not properly writing to a file even if #sDB is defined. See Ruby - Printing a hash to txt file for an example.

Your question is missing essential input data, so there's no way to test our suggested changes.
Here's untested code I'd work from:
def quit_program
puts "Save Changes? y/n"
if gets.chomp.downcase == 'y'
File.write(
#file_name,
#s_db.map{ |k, v| "#{ k }: #{ v.join(',') }" }.join("\n")
)
end
end
Note:
#sDB isn't a proper variable name in Ruby. We use snake_case, not camelCase for variables and method names. ItsAMatterOfReadability. Follow the convention or suffer the wrath of your team members the first time you have a code review.
Don't add empty parenthesis to method names (quit_program()) or calls (gets()) unless it's essential to tell the difference between a variable and a method invocation. You should also never name a variable the same as a method because it'll confuse everyone working on the code, so that should never be a consideration.
Don't create a variable (#changes) you use once and throw away, unless what you're doing is so complex you need to break down the operation into smaller chunks. And, if you're doing that, it'd be a really good candidate for refactoring into separate methods, so again, just don't.
When comparing user-input to something you expect, fold the case of their input to match what you expect. (gets.chomp.downcase == 'y'). It really irritates users to enter "y" and fail because you insisted on "Y".
While you can use File.open to create or write to a file, there's less visual noise to use File.write. open is great when you need to use various options for the mode but for plain text write is sufficient.
The whole block used for writing looks like it can be cleaned up to a single map and join, which coerces the data into an array of strings then into a single string.

Related

How do I delete specific data from a YAML file in Ruby?

FYI I am new to programming :) I am using plain Ruby (not Rails)
I have made a command line app based on some features from The Sims. Users can create Sims (name, gender, life stage, trait) and that info is saved in database.yml.
I am trying to create a method which will allow the user to delete specific Sims from the database. Here's what my YAML file looks like:
---
:id:
:name: Emily
:gender: female
:life_stage: adult
:trait: friendly
---
:id:
:name: Ben
:gender: male
:life_stage: elder
:trait: mean
---
:id:
:name: Josh
:gender: child
:life_stage: adult
:trait: friendly
Here is my Ruby code. (The argument parsed into the method is the name of the Sim to be deleted):
def delete_sim(sim)
log = File.open("../data/database.yml")
YAML::load_stream(log) do |doc|
if sim == doc[:id][:name]
delete.(doc)
puts "You've successfully deleted #{sim}"
else
puts "Error"
break
end
end
end
When I run the code, it displays the else error, which seems to indicate that line 4 isn't right. But what I'm mainly hoping to find out is the correct command to delete data from YAML (line 5). I can't seem to find what I'm looking for in the YAML documentation.
If the solution is obvious to someone here, I'd much appreciate your insight :)
Thanks!
Problem
You have three YAML documents in your file, not a single document. When you load your file as a YAML stream, you actually get a single Array containing three Hash objects. For example:
require 'yaml'
yaml = YAML.load_stream File.read('database.yml')
#=> [{:id=>{:name=>"Emily", :gender=>"female", :life_stage=>"adult", :trait=>"friendly"}}, {:id=>{:name=>"Ben", :gender=>"male", :life_stage=>"elder", :trait=>"mean"}}, {:id=>{:name=>"Josh", :gender=>"child", :life_stage=>"adult", :trait=>"friendly"}}]
yaml.class
#=> Array
yaml.count
#=> 3
yaml.first.class
#=> Hash
You have other problems with your code as well, but understanding the object class that you're working with is really the most important step.
Solution
To remove an item from this type of collection, you'll have to iterate through each element and then write the modified results back out as separate YAML documents. For example:
require 'yaml'
DB = 'database.yml'
def write_database sims
File.open(DB, 'w') { |f| f.puts sims.map(&:to_yaml) }
sims
end
def delete_sim name
sims = YAML.load_stream File.read(DB)
raise ArgumentError, "no sim found: #{name}" if
sims.filter { |sim| sim.dig(:id, :name) == name }.none?
sims.reject! { |sim| sim[:id][:name] == name }
write_database sims
end
delete_sim 'Ben'
#=> [{:id=>{:name=>"Emily", :gender=>"female", :life_stage=>"adult", :trait=>"friendly"}}, {:id=>{:name=>"Josh", :gender=>"child", :life_stage=>"adult", :trait=>"friendly"}}]
delete_sim 'Dave'
#=> ArgumentError (no sim found: Dave)
If you're using this as a database, you should also consider refactoring to a YAML::Store instead of a concatenated stream of YAML documents to take advantage of easier reading and writing, to provide support for transactions, and to handle file locking and prevent race conditions. However, the code above will solve the problem you're currently experiencing.
When I run the code, it displays the else error, which seems to indicate that line 4 isn't right.
Line 4 is correct. However, consider what happens if sim is not 'Emily'?
As soon as the sim that was passed as an argument does not match the name you are looking for, you go into the else branch and then break out of the loop. That means you never look at the second, third, etc. document in your YAML stream.
Either the first sim is the one you are looking for, then you delete it, or the first sim is not the one you are looking for, then you print an error and terminate the loop.
The simplest possible modification I can think of that should fix your code, would be something like this:
def delete_sim(sim)
log = File.open('../data/database.yml')
YAML::load_stream(log) do |doc|
next unless sim == doc[:id][:name]
delete.(doc)
puts "You've successfully deleted #{sim}"
return
end
puts "Error"
end

How to save a hash in a file and use it later in ruby?

So i got in big troubles with this exam at university because i am stuck with a part in my ruby code. I just can't figure out how
" If the user presses 2 the program shall ask for an employee number and afterwards search for the employee. If the program finds it, then print and if not, print a message saying it doesn’t have it."
My problem is that i'm not sure that the information is saved corectly in the file. But if it is... the problem is that the hash i've made isn't taking the information that already is saved in the file and only works with the information it has received last.
puts "Insert Registration number \n"
search = gets.chomp
hash = Hash.new()
hash = {(regnr) => (name)}
hash.each do |key, value|
puts "#{key} \t | \t #{value}"
end
search =~ File.new("employees.txt", "r")
if hash.has_key? (search)
print "The person you were looking for is "
puts hash [search]
else
puts "He isn't one of our employees"
end
I have to tell you guys that i have only been coding for one month and the school isn't taking me easy...
I'd recommend using yaml. Take a look around the web for some examples on using YAML. It's a structured markup that can represent hashes. You can easily dump and load simple ruby objects like hashes and arrays.
require 'yaml'
parsed = begin
employee_hash = YAML.load(File.open("employees.yml"))
rescue ArgumentError => e
puts "Could not parse YAML: #{e.message}"
end

I an getting an "Undefined method 'new' for.... (A number that changes each time)"

I made a simple program with a single method and I'm trying to test it, but I keep getting this weird error, and I have no idea why it keeps happening.
Here's my code for the only method I wrote:
def make_database(lines)
i = 0
foods = hash.new()
while i < lines.length do
lines[i] = lines[i].chomp()
words = lines[i].split(',')
if(words[1].casecmp("b") == 0)
foods[words[0]] = words[3]
end
end
return foods
end
And then here's what I have for calling the method (Inside the same program).
if __FILE__ == $PROGRAM_NAME
lines = []
$stdin.each { |line| lines << line}
foods = make_database(lines).new
puts foods
end
I am painfully confused, especially since it gives me a different random number for each "Undefined method 'new' for (Random number)".
It's a simple mistake. hash calls a method on the current object that returns a number used by the Hash structure for indexing entries, where Hash is the hash class you're probably intending:
foods = Hash.new()
Or more succinctly:
foods = { }
It's ideal to use { } in place of Hash.new unless you need to specify things like defaults, as is the case with:
Hash.new(0)
Where all values are initialized to 0 by default. This can be useful when creating simple counters.
Ruby classes are identified by leading capital letters to avoid confusion like this. Once you get used to the syntax you'll have an easier time spotting mistakes like that.
Note that when writing Ruby code you will almost always omit braces/brackets on empty argument lists. That is x() is expressed simply as x. This keeps code more readable, especially when chaining, like x.y.z instead of x().y().z()
Other things to note include being able to read in all lines with readlines instead of what you have there where you manually compose it. Try:
make_database($stdin.readlines.map(&:chomp))
A more aggressive refactoring of your code looks like this:
def make_database(lines)
# Define a Hash based on key/value pairs in an Array...
Hash[
# ...where these pairs are based on the input lines...
lines.map do |line|
# ...which have comma-separated components.
line.split(',')
end.reject do |key, flag, _, value|
# Pick out only those that have the right flag.
flag.downcase == 'b'
end.map do |key, flag, _, value|
# Convert to a simple key/value pair array
[ key, value ]
end
]
end
That might be a little hard to follow, but once you get the hang of chaining together a series of otherwise simple operations your Ruby code will be a lot more flexible and far easier to read.

Puts arrays in file using ruby

This is a part of my file:
project(':facebook-android-sdk-3-6-0').projectDir = new File('facebook-android-sdk-3-6-0/facebook-android-sdk-3.6.0/facebook')
project(':Forecast-master').projectDir = new File('forecast-master/Forecast-master/Forecast')
project(':headerListView').projectDir = new File('headerlistview/headerListView')
project(':library-sliding-menu').projectDir = new File('library-sliding-menu/library-sliding-menu')
I need to extract the names of the libs. This is my ruby function:
def GetArray
out_file = File.new("./out.txt", "w")
File.foreach("./file.txt") do |line|
l=line.scan(/project\(\'\:(.*)\'\).projectDir/)
File.open(out_file, "w") do |f|
l.each do |ch|
f.write("#{ch}\n")
end
end
puts "#{l} "
end
end
My function returns this:
[]
[["CoverFlowLibrary"]]
[["Android-RSS-Reader-Library-master"]]
[["library"]]
[["facebook-android-sdk-3-6-0"]]
[["Forecast-master"]]
My problem is that I find nothing in out_file. How can I write to a file? Otherwise, I only need to get the name of the libs in the file.
Meditate on this:
"project(':facebook-android-sdk-3-6-0').projectDir'".scan(/project\(\'\:(.*)\'\).projectDir/)
# => [["facebook-android-sdk-3-6-0"]]
When scan sees the capturing (...), it will create a sub-array. That's not what you want. The knee-jerk reaction is to flatten the resulting array of arrays but that's really just a band-aid on the code because you chose the wrong method.
Instead consider this:
"project(':facebook-android-sdk-3-6-0').projectDir'"[/':([^']+)'/, 1]
# => "facebook-android-sdk-3-6-0"
This is using String's [] method to apply a regular expression with a capture and return that captured text. No sub-arrays are created.
scan is powerful and definitely has its place, but not for this sort of "find one thing" parsing.
Regarding your code, I'd do something like this untested code:
def get_array
File.new('./out.txt', 'w') do |out_file|
File.foreach('./file.txt') do |line|
l = line[/':([^']+)'/, 1]
out_file.puts l
puts l
end
end
end
Methods in Ruby are NOT camelCase, they're snake_case. Constants, like classes, start with a capital letter and are CamelCase. Don't go all Java on us, especially if you want to write code for a living. So GetArray should be get_array. Also, don't start methods with "get_", and don't call it array; Use to_a to be idiomatic.
When building a regular expression start simple and do your best to keep it simple. It's a maintainability thing and helps to reduce insanity. /':([^']+)'/ is a lot easier to read and understand, and accomplishes the same as your much-too-complex pattern. Regular expression engines are greedy and lazy and want to do as little work as possible, which is sometimes totally evil, but once you understand what they're doing it's possible to write very small/succinct patterns to accomplish big things.
Breaking it down, it basically says "find the first ': then start capturing text until the next ', which is what you're looking for. project( can be ignored as can ).projectDir.
And actually,
/':([^']+)'/
could really be written
/:([^']+)'/
but I felt generous and looked for the leading ' too.
The problem is that you're opening the file twice: once in:
out_file = File.new("./out.txt", "w")
and then once for each line:
File.open(out_file, "w") do |f| ...
Try this instead:
def GetArray
File.open("./out.txt", "w") do |f|
File.foreach("./file.txt") do |line|
l=line.scan(/project\(\'\:(.*)\'\).projectDir/)
l.each do |ch|
f.write("#{ch}\n")
end # l.each
end # File.foreach
end # File.open
end # def GetArray

ruby simple substitutions

very new to Ruby, I've got the following situation. I have a file with values separated by new lines, they look like this:
18917
18927
18929
...
I want to prepend a folder path to all of them, then grab the first 2 characters and prepend that as well, then the value in the file and then append a '.jpg' at the end so they would end up looking like this:
path/to/foler/18/18917.jpg
So I've code this ruby code:
folder = "/path/to/folder"
lines = File.readlines("values.csv")
images = lines.collect.to_s.gsub("\n", ".jpg,")
images.split(',').collect { |dogtag | puts "bad dog: #{folder}/#{dogtag[0,2]}/#{dogtag}" }
Now, this almost works, the part that is not working is the grabbing of the first 2 characters. I also tried it with the method outside quotes (and without the #{} of course) but it just produces an empty result.
Could someone please point out my folly?
Eventually I want to delete those images but I'm guessing that substituting 'File.delete' for 'puts' above would do the trick?
As usual, thanks in advance for taking the time to look at this.
You don't seem to be understanding what collect does.
I would rewrite your snippet like this:
File.read("values.csv").each do |line|
puts "bad dog: /path/to/folder/#{line[0,2]}/#{line.chomp}.jpg"
end
-- Update for last comment: --
If you don't want to use an if statement to check if a file exists before deleting it, you have two option (AFAIK).
Use rescue:
File.read("values.csv").each do |line|
begin
File.delete "/path/to/folder/#{line[0,2]}/#{line.chomp}.jpg"
rescue Errno::ENOENT
next
end
end
Or extend the File class with a delete_if_exists method. You can just put this at the top of your script:
class File
def self.delete_if_exists(f)
if exists?(f)
delete(f)
end
end
end
With this, you can do:
File.read("values.csv").each do |line|
File.delete_if_exists "/path/to/folder/#{line[0,2]}/#{line.chomp}.jpg"
end

Resources