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 }
Related
Given a string of digits, I am trying to insert '-' between odd numbers and '*' between even numbers. The solution below:
def DashInsertII(num)
num = num.chars.map(&:to_i)
groups = num.slice_when {|x,y| x.odd? && y.even? || x.even? && y.odd?}.to_a
puts groups.to_s
groups.map! do |array|
if array[0].odd?
array.join(" ").gsub(" ", "-")
else
array.join(" ").gsub(" ", "*")
end
end
d = %w{- *}
puts groups.join.chars.to_s
groups = groups.join.chars
# Have to account for 0 because Coderbyte thinks 0 is neither even nor odd, which is false.
groups.each_with_index do |char,index|
if d.include? char
if (groups[index-1] == "0" || groups[index+1] == "0")
groups.delete_at(index)
end
end
end
groups.join
end
is very convoluted, and I was wondering if I could do something like this:
"99946".gsub(/[13579][13579]/) {|s,x| s+"-"+x}
where s is the first odd, x the second. Usually when I substitute, I replace the matched term, but here I want to keep the matched term and insert a character between the pattern. This would make this problem much simpler.
This will work for you:
"99946".gsub(/[13579]+/) {|s| s.split("").join("-") }
# => "9-9-946"
It's roughly similar to what you tried. It captures multiple consecutive odd digits, and uses the gsub block to split and then join them separated by the "-".
This will include both solutions working together:
"99946".gsub(/[13579]+/) {|s| s.split("").join("-") }.gsub(/[02468]+/) {|s| s.split("").join("*") }
# => "9-9-94*6"
The accepted answer illustrates well the logic required to solve the problem. However, I'd like to suggest that in production code that it be simplified somewhat so that it is easier to read and understand.
In particular, we are doing the same thing twice with different arguments, so it would be helpful to the reader to make that obvious, by writing a method or lambda that both uses call. For example:
do_pair = ->(string, regex, delimiter) do
string.gsub(regex) { |s| s.chars.join(delimiter) }
end
Then, one can call it like this:
do_pair.(do_pair.('999434432', /[13579]+/, '-'), /['02468']+/, '*')
This could be simplified even further:
do_pair = ->(string, odd_or_even) do
regex = (odd_or_even == :odd) ? /[13579]+/ : /['02468']+/
delimiter = (odd_or_even == :odd) ? '-' : '*'
string.gsub(regex) { |s| s.chars.join(delimiter) }
end
One advantage to this approach is that it makes obvious both the fact that we are processing two cases, odd and even, and the values we are using for those two cases. It can then be called like this:
do_pair.(do_pair.('999434432', :odd), :even)
This could also be done in a method, of course, and that would be fine. The reason I suggested a lambda is that it's pretty minimal logic and it is used in only one (albeit compound) expression in a single method.
This is admittedly more verbose, but breaks down the logic for the reader into more easily digestible chunks, reducing the cognitive cost of understanding it.
The ordinary way to do that is:
"99946"
.gsub(/(?<=[13579])(?=[13579])/, "-")
.gsub(/(?<=[2468])(?=[2468])/, "*")
# => "9-9-94*6"
or
"99946".gsub(/(?<=[13579])()(?=[13579])|(?<=[2468])()(?=[2468])/){$1 ? "-" : "*"}
# => "9-9-94*6"
"2899946".each_char.chunk { |c| c.to_i.even? }.map { |even, arr|
arr.join(even ? '*' : '-') }.join
#=> "2*89-9-94*6"
The steps:
enum0 = "2899946".each_char
#=> #<Enumerator: "2899946":each_char>
We can convert enum0 to an array to see the elements it will generate:
enum0.to_a
#=> ["2", "8", "9", "9", "9", "4", "6"]
Continuing,
enum1 = enum0.chunk { |c| c.to_i.even? }
#=> #<Enumerator: #<Enumerator::Generator:0x007fa733024b58>:each>
enum1.to_a
#=> [[true, ["2", "8"]], [false, ["9", "9", "9"]], [true, ["4", "6"]]]
a = enum1.map { |even, arr| arr.join(even ? '*' : '-') }
#=> ["2*8", "9-9-9", "4*6"]
a.join
#=> "2*89-9-94*6"
In this code:
b = ["here", "are", "things"]
b.inject { |str, v| str+="#{v} " }
# => "hereare things "
shouldn't the return value be "here are things"? I assume it's passing the first value to the accumulator str. Is there a way to return "here are things "?
I'd assume it's passing in the first value to the accumulator
Correct, because initial value is not defined, first element of your collection becomes the initial value. Fix? Provide the initial value:
b = ['here', 'are', 'things']
b.inject('') { |memo, elem| memo + "#{elem} " } # => "here are things "
Add the space before the word, like this. This does not have trailing space in the result.
["here", "are", "things"].inject { |str, v| str+=" #{v}" }
#=> "here are things"
You could also do something like below and still not have trailing space
['here', 'are', 'things'].inject { |m, e| "#{m} #{e}" }
#=> "here are things"
I’m using the PaperTrail gem to track changes in my Job model. Each time I change a record, the object is serialized and stored inside a changeset field on my Version model.
To display the values that have changed, I’m deserializing the object and iterating through it using map. I need help creating a method that extracts all the keys and values from this hash, regardless of whether they’re nested or not. The nesting will never be more than one level deep.
Here is my current code:
# deserializing the object into a hash
json_string = v.changeset.to_json
serialized_json = JSON.parse(json_string)
# current method to extract keys with values, but doesn’t pick up nested ones
serialized_json.map { |k,v|
puts k.titleize
puts v[1].present? ? v[1] : '<empty>'
}
# serialized_json (1st output)
{
"name" => [
[0] nil,
[1] "Epping Forest"
],
"job_address" => [
[0] nil,
[1] "51 Epping Forest, Essex, ES1 SAD"
]
}
# serialized_json (2nd output)
{
"quote" => [
[0] {
"quote" => {
"url" => nil
}
},
[1] {
"quote" => {
"url" => "/tmp/uploads/1433181671-22986-9554/file.pdf"
}
}
]
}
The values I’d like to extract from my method are:
"Name"
"Epping Forest"
"Quote Url"
"/tmp/uploads/1433181671-22986-9554/file.pdf"
In your below code, I assume you're putting [0] and [1] as index numbers to help the readers of your code, as it is not valid Ruby syntax.
"name" => [
[0] nil,
[1] "Epping Forest"
],
As for extracting 1 level deep keys and values, since it's a bit unclear to me how you would like your output formatted, I'ma ssuming a 1-D array is fine...in which case, something like this would give you the keys and values:
serialized_json hash w/valid Ruby syntax
{
"name" => [
nil,
"Epping Forest"
],
"job_address" => [
nil,
"51 Epping Forest, Essex, ES1 SAD"
]
}
Then:
serialized_json.map { |k, v| [k, v] }.flatten.reject { |x| x.nil? }
outputs:
["name", "Epping Forest", "job_address", "51 Epping Forest, Essex, ES1 SAD"]
I hope this helps :)
#str = ""
def hash_checks(hash)
hash.each do |k,v|
if v.is_a?(Array)
v.each_with_index do |element, index|
if element.is_a?(Hash)
#str = element[index] + #str if hash_check(element)
else
#str << k.titleize + "\n" + element + "\n" unless element.nil?
end
end
elsif v.is_a?(Hash)
#str << k.titleize if hash_check(v)
end
end
puts #str
end
def hash_check(hash)
add_tag = false
hash.each do |k,v|
if v.is_a?(Hash)
#str = k.titleize + " " + #str if hash_check(v)
else
#str << k.titleize + "\n" + v + "\n" unless v.nil?
add_tag = true
end
end
add_tag
end
I've excluded the '<empty>' value for nil because the data you would want to extract is the one that matters.
There's an inconsistency on the data you want though, since on your first serialized json, there are 2 non-nil values, but you just want one.
This method includes both, and runs even for multi-level nests :)
This question already has answers here:
Ruby recursive map of a hash of objects
(2 answers)
Closed 8 years ago.
I have a hash object that could be arbitrarily large - keys are always strings, but values could be strings, arrays, or other hashes. I want to recursively walk through the object and if the value of any particular key (or the value of any array) is a string, I want to strip line endings and leading and trailing whitespace (\r\n, \t, etc")
Do I need to write this algorithm myself or is there some faster ruby-esque way to do it?
You will need to write it yourself. One way to do it is:
def strip_hash_values!(hash)
hash.each do |k, v|
case v
when String
v.strip!
when Array
v.each {|av| av.strip!}
when Hash
strip_hash_values!(v)
end
end
end
This method modifies the original hash:
hash = {:a => [" a ", " b ", " c "], :b => {:x => "xyz "}, :c => "abc "}
strip_hash_values!(hash)
puts hash
# returns {:b=>{:x=>"xyz"}, :c=>"abc", :a=>["a", "b", "c"]}
This is one way to do it.
Code
def strip_strings(o)
case o
when Hash
o.each do |k,v|
o[k] = case v
when String
v.strip
else
strip_strings(v)
end
end
else # Array
o.map do |e|
case e
when String
e.strip
else
strip_strings(e)
end
end
end
end
Example
h = { a: [b: { c: " cat ", d: [" dog ", {e: " pig " }] }], f: " pig " }
#=> {:a=>[{:b=>{:c=>" cat ", :d=>[" dog ", {:e=>" pig "}]}}], :f=>" pig "}
strip_strings(h)
#=> {:a=>[{:b=>{:c=>"cat", :d=>["dog", {:e=>"pig"}]}}], :f=>"pig"}
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.