How do I delete specific data from a YAML file in Ruby? - 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

Related

Searching if File does exist

I'm programming a music player that reads in data from a 'data.txt' file (not the problem) then stores it an array with records in it called Album, which includes title, artist.... and tracks. In the tracks record, it includes the track name and track location.
In the code below, im trying to search for the track, for example \music\album1\song_name1.mp3, but this is returning as false in the IF statement event though the file exits.
def playTrack(track, album)
if File.file?(album.tracks[track].location)
puts "Found"
#song = Gosu::Song.new(album.tracks[track].location)
#song.play
else
puts "Not found"
puts album.tracks[track].location
end
end
Yeah. Using p instead of puts help me see that there was a \n after the string. Everything works fine now.
Just did . chomp on the string to fix

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

Outputting hash to text file

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.

Weird File.open behavior when using different closure types in Ruby

I started learning Chef to manage our servers and I stumbled in a very weird (in my opinion) behavior in Ruby. I do not know Ruby so this might be just a misunderstanding from my part.
The error I was getting was
`delete': Permission denied - [some path]/metadata.json (Errno::EACCES)
Since I knew for sure that it was not actually about permissions, the next logical thing was to check for file locking. After digging a bit through the relevant code, I discovered that there is a method that produces a checksum for each file.
load_root
file_metadata
checksum
md5_checksum_for_file
generate_md5_checksum_for_file
checksum_file
def checksum_file(file, digest)
File.open(file, 'rb') { |f| checksum_io(f, digest) }
end
def checksum_io(io, digest)
while chunk = io.read(1024 * 8)
digest.update(chunk)
end
digest.hexdigest
end
Having found that, I searched a bit and found an answer about closing files in Ruby and it seemed that the code was actually fine... but it was not. I tried to change the method to the
"block format" and it worked without error:
def checksum_file(file, digest)
File.open(file, 'rb') do |f|
checksum_io(f, digest)
end
end
Can someone please explain the difference between the two versions of the code?
-- Edit --
It seems that this problem occurs only in Windows and maybe only when using the ruby provided by ChefDK 0.3.0 :
ruby 2.0.0p451 (2014-02-24) [i386-mingw32]
Answer of your question
Can someone please explain the difference between the two versions of the code?
Block always return something so do end and and { ... } dose't really matter This is just personnel programming preference.
There are two different convention with block i'm going to dispense
to you now and it up to you which religion you want to subscribe to.
First religion says that
when you have a single line or single line block you would use the curly braces And if you have a multi line block you would use do and end.
words.each { |word| puts word } # single line or single line block
words.each do |word| # multi line block
puts word
p 1
end
Second religion says that
If your block simply does something has side effect and you dont care about the return value you might put do and end
words.each do |word|
puts word
end
where as if you do care about return value you would use { ... }
back_words = words.map{ |word| word.reverse }
hope I answere your question !!!

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