I am generating a script that is outputting information to the console. The information is some kind of statistic with a value. So much like a hash.
So one value's name may be 8 characters long and another is 3. when I am looping through outputting the information with two \t some of the columns aren't aligned correctly.
So for example the output might be as such:
long value name 14
short 12
little 13
tiny 123421
long name again 912421
I want all the values lined up correctly. Right now I am doing this:
puts "#{value_name} - \t\t #{value}"
How could I say for long names, to only use one tab? Or is there another solution?
Provided you know the maximum length to be no more than 20 characters:
printf "%-20s %s\n", value_name, value
If you want to make it more dynamic, something like this should work nicely:
longest_key = data_hash.keys.max_by(&:length)
data_hash.each do |key, value|
printf "%-#{longest_key.length}s %s\n", key, value
end
There is usually a %10s kind of printf scheme that formats nicely.
However, I have not used ruby at all, so you need to check that.
Yes, there is printf with formatting.
The above example should right align in a space of 10 chars.
You can format based on your widest field in the column.
printf ([port, ]format, arg...)
Prints arguments formatted according to the format like sprintf. If the first argument is the instance of the IO or its subclass, print redirected to that object. the default is the value of $stdout.
String has a built-in ljust for exactly this:
x = {"foo"=>37, "something long"=>42, "between"=>99}
x.each { |k, v| puts "#{k.ljust(20)} #{v}" }
# Outputs:
# foo 37
# something long 42
# between 99
Or, if you want tabs, you can do a little math (assuming tab display width of 8) and write a short display function:
def tab_pad(label, tab_stop = 4)
label_tabs = label.length / 8
label.ljust(label.length + tab_stop - label_tabs, "\t")
end
x.each { |k, v| puts "#{tab_pad(k)}#{v}" }
# Outputs:
# foo 37
# something long 42
# between 99
There was few bugs in it before, but now you can use most of printf syntax with % operator:
1.9.3-p194 :025 > " %-20s %05d" % ['hello', 12]
=> " hello 00012"
Of course you can use precalculated width too:
1.9.3-p194 :030 > "%-#{width}s %05x" % ['hello', 12]
=> "hello 0000c"
I wrote a thing
Automatically detects column widths
Spaces with spaces
Array of arrays [[],[],...] or array of hashes [{},{},...]
Does not detect columns too wide for console window
lists = [
[ 123, "SDLKFJSLDKFJSLDKFJLSDKJF" ],
[ 123456, "ffff" ],
]
array_maxes
def array_maxes(lists)
lists.reduce([]) do |maxes, list|
list.each_with_index do |value, index|
maxes[index] = [(maxes[index] || 0), value.to_s.length].max
end
maxes
end
end
array_maxes(lists)
# => [6, 24]
puts_arrays_columns
def puts_arrays_columns(lists)
maxes = array_maxes(hashes)
lists.each do |list|
list.each_with_index do |value, index|
print " #{value.to_s.rjust(maxes[index])},"
end
puts
end
end
puts_arrays_columns(lists)
# Output:
# 123, SDLKFJSLDKFJSLDKFJLSDKJF,
# 123456, ffff,
and another thing
hashes = [
{ "id" => 123, "name" => "SDLKFJSLDKFJSLDKFJLSDKJF" },
{ "id" => 123456, "name" => "ffff" },
]
hash_maxes
def hash_maxes(hashes)
hashes.reduce({}) do |maxes, hash|
hash.keys.each do |key|
maxes[key] = [(maxes[key] || 0), key.to_s.length].max
maxes[key] = [(maxes[key] || 0), hash[key].to_s.length].max
end
maxes
end
end
hash_maxes(hashes)
# => {"id"=>6, "name"=>24}
puts_hashes_columns
def puts_hashes_columns(hashes)
maxes = hash_maxes(hashes)
return if hashes.empty?
# Headers
hashes.first.each do |key, value|
print " #{key.to_s.rjust(maxes[key])},"
end
puts
hashes.each do |hash|
hash.each do |key, value|
print " #{value.to_s.rjust(maxes[key])},"
end
puts
end
end
puts_hashes_columns(hashes)
# Output:
# id, name,
# 123, SDLKFJSLDKFJSLDKFJLSDKJF,
# 123456, ffff,
Edit: Fixes hash keys considered in the length.
hashes = [
{ id: 123, name: "DLKFJSDLKFJSLDKFJSDF", asdfasdf: :a },
{ id: 123456, name: "ffff", asdfasdf: :ab },
]
hash_maxes(hashes)
# => {:id=>6, :name=>20, :asdfasdf=>8}
Want to whitelist columns columns?
hashes.map{ |h| h.slice(:id, :name) }
# => [
# { id: 123, name: "DLKFJSDLKFJSLDKFJSDF" },
# { id: 123456, name: "ffff" },
#]
For future reference and people who look at this or find it... Use a gem. I suggest https://github.com/wbailey/command_line_reporter
You typically don't want to use tabs, you want to use spaces and essentially setup your "columns" your self or else you run into these types of problems.
Related
so I know how I can iterate over and make array within hash
travel=["Round Trip Ticket Price:", "Price of Accommodation:", "Number of checked bags:"]
(1..3).each_with_object({}) do |trip, travels|
puts "Please input the following for trip # #{trip}"
travels["trip #{trip}"]= travel.map { |q| print q; gets.chomp.to_f }
end
==>{"trip 1"=>[100.0, 50.0, 1.0], "trip 2"=>[200.0, 100.0, 2.0], "trip 3"=>[300.0, 150.0,
3.0]}
BUT instead I want to iterate over to make three individual hashes within one array.
I want it to look something like this
travels=[{trip_transportation: 100.0, trip_accommodation:50.0, trip_bags:50}
{trip_transportation:200.0, trip_accommodation:100.0, trip_2_bags:100}
{trip_3_transportation:300.0, trip_accommodation:150.0, trip_3_bags:150}]
I am really confused, basically the only thing I want to know how to do is how do I make three separate hashes while using a loop.
I want every hash to represent a trip.
Is that even possible?
travel=[{ prompt: "Round Trip Ticket Price: ",
key: :trip_transportation, type: :float },
{ prompt: "Price of Accommodation : ",
key: :trip_accommodation, type: :float },
{ prompt: "Number of checked bags : ",
key: :trip_bags, type: :int }]
nbr_trips = 3
Suppose that as the following code is run the user were to input the values given in the question's example.
(1..nbr_trips).map do |trip|
puts "Please input the following for trip #{trip}"
travel.map do |h|
print h[:prompt]
s = gets
[h[:key], h[:type] == :float ? s.to_f : s.to_i]
end.to_h
end
#=> [{:trip_transportation=>100.0, :trip_accommodation=>50.0, :trip_bags=>1},
# {:trip_transportation=>200.0, :trip_accommodation=>100.0, :trip_bags=>2},
# {:trip_transportation=>300.0, :trip_accommodation=>150.0, :trip_bags=>3}]
I see no reason for keys to have different names for different trips (e.g., :trip_2_bags and trip_3_bags, rather than simply trip_bags for all trips).
Using an Hash for setting up, similar to Cary Swoveland's answer and similar to my answer here: https://stackoverflow.com/a/58485997/5239030
travel = { trip_transportation: { question: 'Round Trip Ticket Price:', convert: 'to_f' },
trip_accommodation: { question: 'Price of Accommodation:', convert: 'to_f' },
trip_bags: { question: 'Number of checked bags:', convert: 'to_i' } }
n = 2
res = (1..n).map do # |n| # uncomment if (*)
travel.map.with_object({}) do |(k, v), h|
puts v[:question]
# k = k.to_s.split('_').insert(1, n).join('_').to_sym # uncomment if (*)
h[k] = gets.send(v[:convert])
end
end
res
#=> [{:trip_transportation=>10.0, :trip_accommodation=>11.0, :trip_bags=>1}, {:trip_transportation=>20.0, :trip_accommodation=>22.0, :trip_bags=>2}]
(*) Uncomment if you want the result to appear like:
#=> [{:trip_1_transportation=>10.0, :trip_1_accommodation=>11.0, :trip_1_bags=>1}, {:trip_2_transportation=>20.0, :trip_2_accommodation=>22.0, :trip_2_bags=>2}]
My apologies for the potentially stupid question, I'm an absolute beginner to Ruby and code in general.
I have set up a hash with some predetermined values. I want to ask the user for input, if that input matches an existing key, I want the corresponding value to be updated (+ 1, in this case). Then I want to print all the current up-to-date values.
hash = {"apple": 6, "banana": 2, "carrot": 3}
order = gets.chomp.downcase
hash.each do |key, value|
if key.to_s == order
value += 1
puts "Your order includes: #{value} #{key}."
end
end
My problem is that I only know how to print a single key value pair.
E.g. if the user inputs "apple", I'd like the output to say "Your order includes: 7 apple, 2 banana, 3 carrot."
hash = {apple: 6, banana: 2, carrot: 3}
order = gets.chomp.downcase.to_sym
hash[order] = hash.fetch(order, 0) + 1
puts "Your order includes: " + hash.map { |k, v| "#{v} #{k}" }.join(", ")
Some notes:
your hash initialization hash = {"apple": 6, "banana": 2, "carrot": 3}. the keys of your hash seem strings, but if you use that syntax with the colon, they become symbols. So, you have two choice. this syntax:
hash = {"apple" => 6, "banana" => 2, "carrot" => 3}
or you can use symbols as I did and convert the user input in a symbol
what's really cool about hash is that you don't need to iterate through the elements to find what you're looking for. There's a mapping between keys and values, so it's easy find and update a value
in the third row, I'm dealing with the fact that the key could not be in the hash, I used fetch to have 0 in that case. then, I increment and I assign back to that key
The question does not specify if you want to mutate the initial hash, so I suppose you do. Then the following will do.
hash = Hash.new(0).merge(apple: 6, banana: 2, carrot: 3)
hash[gets.chomp.downcase.to_sym] += 1
puts "Your order includes: " <<
hash.map { |k, v| [v, k].join(' ') }.join(', ')
or:
puts hash.reduce("Your order includes: ") { |acc, (k, v)|
acc << "#{v} #{k}, "
}[0..-3]
Consider to initialize the hash providing a default value (Hash#default)
basket = {'apple' => 6, 'banana' => 2, 'carrot' => 3}
basket.default = 0 # <-- set default 0 for start counting new keys
Define a method to present the data:
def show(basket)
puts "Your order includes:"
basket.each{ |key, value| puts "#{value}: #{key}" }
end
Capture user input in a loop (explanation in comments):
loop do
puts "Place your order:"
order = gets.downcase.chomp # <-- format the input
break if order == '0' # <-- breaks the input loop if this contition is matched
next unless basket.has_key? order # <-- skip to next loop no matches with hash keys or remove this line if you want to count also non initialised keys
basket[order] += 1 # <-- increment by one the key
show(basket) # <-- call the metod to show the basket
end
show(basket)
My file content is
blablabla
Name : 'XYZ'
Age : '30'
Place : 'ABCD'
blablabla
How can I grep for "Name", "Age", "Place" and store name "XYZ", age "30" and place "ABCD" in a hash?
What should be the '?' in this code to get those?
data = {}
name = /Name/
age = /Age/
place = /Place/
read_lines(file) { |l|
case l
when name
data[:name] = ?
when age
data[:age] = ?
when place
data[:place]= ?
end
}
You can use something like this.
data = {}
keys = {:name => "Name", :age => "Age", :place => "Place"}
File.open("test.txt", "r") do |f|
f.each_line do |line|
line.chomp!
keys.each do |hash_key, string|
if line[/#{string}/]
data[hash_key] = line.strip.split(" : ")[-1].gsub("'", "")
break
end
end
end
end
output
p data
# => {:name=>"XYZ", :age=>"30", :place=>"ABCD"}
Strange code, but in this case:
data[:name] = l.split(':')[1] if l.match(name)
when age
data[:age] = l.split(':')[1] if l.match(age)
when place
data[:place]= l.split(':')[1] if l.match(place)
Are you interested in refactoring?
One option is to:
mapping =
[
{ name: :name, pattern: /Name/ },
{ name: :age, pattern: /Age/ },
{ name: :place, pattern: /Place/ }
]
data = str.split(/\r?\n|\r/).map do |line|
mapping.map{|pair|
{ pair[:name] => line.split(' : ')[1].gsub("'", "") } if line.match(pair[:pattern])
}.compact.reduce({}, :merge)
end.reduce({}, :merge)
Suppose we first read the file into a string:
str = File.read('fname')
which is:
str =<<_
blablabla
Name : 'XYZ'
Age : '30'
Place : 'ABCD'
blablabla
_
#=> "blablabla\nName : 'XYZ'\nAge : '30'\nPlace : 'ABCD'\nblablabla\n"
Then use the regex
r = /
^ # match beginning of line
Name\s*:\s*'(.*)'\n # match 'Name`, ':' possibly surrounded by spaces, any number
# of any character in capture group 1, end of line
Age\s*:\s*'(.*)'\n # match 'Age`, ':' possibly surrounded by spaces, any number
# of any character in capture group 2, end of line
Place\s*:\s*'(.*)'\n # match 'Place`, ':' possibly surrounded by spaces, any number
# of any character in capture group 3, end of line
/x # free-spacing regex definition mode
with String#scan to form the hash:
[:name, :age, :place].zip(str.scan(r).first).to_h
#=> {:name=>"XYZ", :age=>"30", :place=>"ABCD"}
I'd do something like this:
str = <<EOT
blablabla
Name : 'XYZ'
Age : '30'
Place : 'ABCD'
blablabla
EOT
str.scan(/(Name|Age|Place)\s+:\s'([^']+)/).to_h # => {"Name"=>"XYZ", "Age"=>"30", "Place"=>"ABCD"}
scan will create sub-arrays if it sees pattern groups in the regular expression. Those make it easy to turn the returned array of arrays into a hash.
If you need to fold the keys to lower-case, or convert them to symbols:
str.scan(/(Name|Age|Place)\s+:\s'([^']+)/)
.map{ |k, v| [k.downcase, v] } # => [["name", "XYZ"], ["age", "30"], ["place", "ABCD"]]
.to_h # => {"name"=>"XYZ", "age"=>"30", "place"=>"ABCD"}
Or:
str.scan(/(Name|Age|Place)\s+:\s'([^']+)/)
.map{ |k, v| [k.downcase.to_sym, v] } # => [[:name, "XYZ"], [:age, "30"], [:place, "ABCD"]]
.to_h # => {:name=>"XYZ", :age=>"30", :place=>"ABCD"}
Or some variation on:
str.scan(/(Name|Age|Place)\s+:\s'([^']+)/)
.each_with_object({}){ |(k,v), h| h[k.downcase.to_sym] = v}
# => {:name=>"XYZ", :age=>"30", :place=>"ABCD"}
If the example string truly is the complete file, and there won't be any other reoccurrence of the key/value pairs, then this will work. If there could be more than one then the resulting hash will not be correct because the subsequent pairs will stomp on the first one. If the file is as you said, then it'll work fine.
As per the question, just wondering how to do this without the use of the Ruby stdlib 'JSON' module (and thus the JSON.pretty_generate method).
So I have an array of hashes that looks like:
[{"h1"=>"a", "h2"=>"b", "h3"=>"c"}, {"h1"=>"d", "h2"=>"e", "h3"=>"f"}]
and I'd like to convert it so that it looks like the following:
[
{
"h1": "a",
"h2": "b",
"h3": "c",
},
{
"h1": "d",
"h2": "e",
"h3": "f",
}
]
I can get the hash-rockets replaced with colon spaces using a simple gsub (array_of_hashes.to_s.gsub!(/=>/, ": ")), but not sure about how to generate it so that it looks like the above example. I had originally thought of doing this use a here-doc approach, but not sure this is the best way, plus i havn't managed to get it working yet either. I'm new to Ruby so apologies if this is obvious! :-)
def to_json_pretty
json_pretty = <<-EOM
[
{
"#{array_of_hashes.each { |hash| puts hash } }"
},
]
EOM
json_pretty
end
In general, working with JSON well without using a library is going to take more than just a few lines of code. That being said, the best way of JSON-ifying things is generally to do it recursively, for example:
def pretty_json(obj)
case obj
when Array
contents = obj.map {|x| pretty_json(x).gsub(/^/, " ") }.join(",\n")
"[\n#{contents}\n]"
when Hash
contents = obj.map {|k, v| "#{pretty_json(k.to_s)}: #{pretty_json(v)}".gsub(/^/, " ") }.join(",\n")
"{\n#{contents}\n}"
else
obj.inspect
end
end
This should work well if you input is exactly in the format you presented and not nested:
a = [{"h1"=>"a", "h2"=>"b", "h3"=>"c"}, {"h1"=>"d", "h2"=>"e", "h3"=>"f"}]
hstart = 0
astart = 0
a.each do |b|
puts "[" if astart == 0
astart+=1
b.each do |key, value|
puts " {" if hstart == 0
hstart += 1
puts " " + key.to_s + ' : ' + value
if hstart % 2 == 0
if hstart == a.collect(&:size).reduce(:+)
puts " }"
else
puts " },\n {"
end
end
end
puts "]" if astart == a.size
end
Output:
[
{
h1 : a
h2 : b
},
{
h3 : c
h1 : d
},
{
h2 : e
h3 : f
}
]
You can take a look at my NeatJSON gem for how I did it. Specifically, look at neatjson.rb, which uses a recursive solution (via a proc).
My code has a lot of variation based on what formatting options you supply, so it obviously does not have to be as complex as this. But the general pattern is to test the type of object supplied to your method/proc, serialize it if it's simple, or (if it's an Array or Hash) re-call the method/proc for each value inside.
Here's a far-simplified version (no indentation, no line wrapping, hard-coded spacing):
def simple_json(object)
js = ->(o) do
case o
when String then o.inspect
when Symbol then o.to_s.inspect
when Numeric then o.to_s
when TrueClass,FalseClass then o.to_s
when NilClass then "null"
when Array then "[ #{o.map{ |v| js[v] }.join ', '} ]"
when Hash then "{ #{o.map{ |k,v| [js[k],js[v]].join ":"}.join ', '} }"
else
raise "I don't know how to deal with #{o.inspect}"
end
end
js[object]
end
puts simple_json({a:1,b:[2,3,4],c:3})
#=> { "a":1, "b":[ 2, 3, 4 ], "c":3 }
so i have the following
[{:item=>"x"}, {:item2=>"x"}, {:item3=>"x"}, {:item=>"x"},{:item3=>"x"}]
I want to get this split into groups,
so each group starts at item and ends at item3, item2 could be missing
ideally i want
{:item=>"x",:item2=>"x",:item3=>"x"} & {:item=>"x",:item3=>"x"}
So in a real example:
An 3 items need to be posted but I get an array from an excel spreadsheet
name: blah
id: blah
color: blah
name: blah
date: blah
size: blah
name: blah
id: blah
date: blah
color: blah
size: blah
I need to post each record, given I have an array like above is there some way to group/split the array on a given hash element key field?
If your data is really delimited by double line break, you should take advantage by splitting first by paragraph, then by line, then by the colon. Then you don't have to worry about missing data and can blindly fill in key/value pairs.
A functional approach, get the indexes where the :items are and split there:
hs = [{:item=>"x"}, {:item2=>"x"}, {:item3=>"x"}, {:item=>"x"},{:item3=>"x"}]
indexes = hs.map.with_index { |h, i| i if h.first[0] == :item }.compact
(indexes + [hs.size]).each_cons(2).map { |from, to| hs[from...to].reduce(:merge) }
#=> [{:item=>"x", :item2=>"x", :item3=>"x"}, {:item=>"x", :item3=>"x"}]
If you prefer a more declarative approach (I do), add some abstractions to your extensions library so you can write:
indexes = hashes.find_indexes { |h| h.first[0] == :item }
hashes.split_at(*indexes.drop(1)).map { |hs| hs.reduce(:merge) }
try this
input = [{:item=>"x"}, {:item2=>"x"}, {:item3=>"x"}, {:item=>"x"},{:item3=>"x"}]
res = []
input.each do |element|
if element.keys.first == :item
res << element
else
res.last.merge! element
end
end
puts puts res.inspect # => [{:item=>"x", :item2=>"x", :item3=>"x"}, {:item=>"x", :item3=>"x"}]
Pure awesomness of Ruby:
arr = [{:item=>"x"}, {:item2=>"x"}, {:item3=>"x"}, {:item=>"x"}, {:item3=>"x"}]
arr.each_slice(3).map { |a| a.inject(&:merge) }
=> [{:item=>"x", :item2=>"x", :item3=>"x"}, {:item=>"x", :item3=>"x"}]