I'm dealing with some JSON which is quite "flexible", so in some cases an object might be an array, sometimes it might be a string, and sometimes it might not exist at all. It's also quite deeply nested.
For example, the API call returns a list of results and each result might have zero, one or many thumbnails. I want to return the thumbnail if there is only one, the first one if there are many, or nil if there are none.
Is there an easy way to do this with Ruby's JSON library?
At the moment, I'm doing something like this:
def get_thumbnail
if #json['results']
result = #json['results'].first
thumbnail = result['thumbnail']
if thumbnail
if thumbnail.class == String
thumbnail
elsif thumbnail.class == Array
thumbnail.first
else
nil
end
else
nil
end
end
end
Is there a more tolerant way to access the JSON, other than having to check the presence and type of each 'level' I'm accessing?
Thanks in advance.
You can do it by forcing things the way you want to operate on them. This way you avoid having to constantly check for the presence of things and can just work with what is there or reasonable defaults.
def thumbnail(json)
thumbnail_record(json).first
end
def thumbnail_record(json)
Array(first_record(json)['thumbnail'])
end
def first_record(json)
results(json).first || {}
end
def results(json)
Array(json['results'])
end
I've ended up using this dig method which someone suggested, to get around the NoMethodFound errors when chaining hash access:
class Hash
def dig(*path)
path.inject(self) do |location, key|
location.respond_to?(:keys) ? location[key] : nil
end
end
end
json = JSON.parse('{ "foo": [1, 2, 3, 4] }')
json.dig("foo") # Gives [1, 2, 3, 4]
json.dig("nonsense", "bar") # Gives nil
Related
In Ruby, I'd like to know how best to handle getting a variable from a json hash and then proceeding only if it exists, else raise an error.
So here is some example code:
digest = cover_id(map)
def cover_id(a_map)
cover_from_map = a_map['metadata']['cover'][0]['#']
a_map['resources'].find {|resource| resource['id']==cover_from_map}['-major-md5-digest']
end
Looking at this there are 2 things I'm not clear how best to handle:
If firstly song_map['metadata']['cover'][0]['#'] did not exist - is it just a case of using song_map.key?(['metadata']['cover'][0]['#']). I feel using this means I've duplicated code?
Instead of hardcoding that 0 is there any way I can just say get the first?
Basically, from what I know now, I was thinking:
digest = cover_id(map) rescue nil
def cover_id(a_map)
unless a_map['metadata']['cover'][0]['#'] return nil
cover_from_map = a_map['metadata']['cover'][0]['#']
a_map['resources'].find {|resource| resource['id']==cover_from_map}['-major-md5-digest']
end
But I dont think that would handle if a_map['metadata']['cover'][0]['#'] didn't actually exist.
Any help appreciated.
Check method dig of Hash:
https://ruby-doc.org/core-2.3.0_preview1/Hash.html#method-i-dig
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
Does Ruby's Enumerable offer a better way to do the following?
output = things
.find { |thing| thing.expensive_transform.meets_condition? }
.expensive_transform
Enumerable#find is great for finding an element in an enumerable, but returns the original element, not the return value of the block, so any work done is lost.
Of course there are ugly ways of accomplishing this...
Side effects
def constly_find(things)
output = nil
things.each do |thing|
expensive_thing = thing.expensive_transform
if expensive_thing.meets_condition?
output = expensive_thing
break
end
end
output
end
Returning from a block
This is the alternative I'm trying to refactor
def costly_find(things)
things.each do |thing|
expensive_thing = thing.expensive_transform
return expensive_thing if expensive_thing.meets_condition?
end
nil
end
each.lazy.map.find
def costly_find(things)
things
.each
.lazy
.map(&:expensive_transform)
.find(&:meets_condition?)
end
Is there something better?
Of course there are ugly ways of accomplishing this...
If you had a cheap operation, you'd just use:
collection.map(&:operation).find(&:condition?)
To make Ruby call operation only "on a as-needed basis" (as the documentation says), you can simply prepend lazy:
collection.lazy.map(&:operation).find(&:condition?)
I don't think this is ugly at all—quite the contrary— it looks elegant to me.
Applied to your code:
def costly_find(things)
things.lazy.map(&:expensive_transform).find(&:meets_condition?)
end
I would be inclined to create an enumerator that generates values thing.expensive_transform and then make that the receiver for find with meets_condition? in find's block. For one, I like the way that reads.
Code
def costly_find(things)
Enumerator.new { |y| things.each { |thing| y << thing.expensive_transform } }.
find(&:meets_condition?)
end
Example
class Thing
attr_reader :value
def initialize(value)
#value = value
end
def expensive_transform
self.class.new(value*2)
end
def meets_condition?
value == 12
end
end
things = [1,3,6,4].map { |n| Thing.new(n) }
#=> [#<Thing:0x00000001e90b78 #value=1>, #<Thing:0x00000001e90b28 #value=3>,
# #<Thing:0x00000001e90ad8 #value=6>, #<Thing:0x00000001e90ab0 #value=4>]
costly_find(things)
#=> #<Thing:0x00000001e8a3b8 #value=12>
In the example I have assumed that expensive_things and things are instances of the same class, but if that is not the case the code would need to be modified in the obvious way.
I don't think there is a "obvious best general solution" for your problem, which is also simple to use. You have two procedures involved (expensive_transform and meets_condition?), and you also would need - if this were a library method to use - as a third parameter the value to return, if no transformed element meets the condition. You return nil in this case, but in a general solution, expensive_transform might also yield nil, and only the caller knows what unique value would indicate that the condition as not been met.
Hence, a possible solution within Enumerable would have the signature
class Enumerable
def find_transformed(default_return_value, transform_proc, condition_proc)
...
end
end
or something similar, so this is not particularily elegant either.
You could do it with a single block, if you agree to merge the semantics of the two procedures into one: You have only one procedure, which calculates the transformed value and tests it. If the test succeeds, it returns the transformed value, and if it fails, it returns the default value:
class Enumerable
def find_by(default_value, &block)
result = default_value
each do |element|
result = block.call(element)
break if result != default_value
end
end
result
end
You would use it in your case like this:
my_collection.find_by(nil) do |el|
transformed_value = expensive_transform(el)
meets_condition?(transformed_value) ? transformed_value : nil
end
I'm not sure whether this is really intuitive to use...
I need to find out if data[i][j][k] element exists, but I don't know if data[i] or data[i][j] not nil themselves.
If I just data[i][j][k].nil?, it throw undefined method [] for nil:NilClass if data[i] or data[i][j] is nil
So, I am using now
unless data[i].nil? or data[i][j].nil? or data[i][j][k].nil?
# do something with data[i][j][k]
end
But it is somehow cumbersome.
Is there any other way to check if data[i][j][k] exists without data[i].nil? or data[i][j].nil? or data[i][j][k].nil? ?
I usually do:
unless (data[i][j][k] rescue false)
# do something
end
Here are three different alternatives:
Shorten it
You can shorten it slightly by using "!" instead of .nil?:
!data[i] or !data[i][j] or !data[i][j][k]
You could get rid of the repetition by doing this:
((data[i] || [])[j] || [])[k].nil?
Abstract away these details
Both of the code snippets above are nasty enough that I would probably not write them more than once in a code base.
A three-dimensional array seems complicated enough that you shouldn't be accessing it directly in lots of places in your code. You should consider wrapping it inside an object with an appropriate name:
class My3DWorld
def initialize
# set up #data
end
# Gets the point or returns nil if it doesn't exist.
def get_point(i, j, k)
#data[i] && #data[i][j] && #data[i][j][k]
end
end
Use a hash instead
However, ultimately, I wonder whether you really need a 3D array. Another more Ruby-like way to implement this data structure would be to use a hash and use i,j,k coordinate tuples as the keys. Unless this is a huge structure and you need the performance characteristics of a 3D array, I recommend looking at my other answer here:
https://stackoverflow.com/a/20600345/28128
The new feature "refinements" is an option:
module ResponsiveNil
refine NilClass do
def [](obj)
nil
end
end
end
using ResponsiveNil
a = [[1]]
p a[2][3][4] #=> nil
You can shorten slightly to
if data[i] && data[i][j] && data[i][j][k]
# do something with data[i][j][k]
end
You can also you the "andand" gem which allows you to write:
data[i].andand[j].andand[k]
If you are willing to monkey patch Array, you could define a method to enable this, such as:
class Array
def index_series(*args)
result = self
args.each do |key|
result = result[key]
return nil if result.nil?
end
result
end
end
which would let you do:
data.index_series(i, j, k)
The following permits any amount of nesting, and allows for the possibility that an element of the array has a value of nil:
def element_exists?(arr, *indices)
if arr.is_a? Array
return true if indices.empty?
return false if arr.size <= (i = indices.pop)
element_exists?(arr[i], *indices)
else
indices.empty?
end
end
data = [[0,1],[2,nil]]
element_exists?(data) # => true
element_exists?(data, 1) # => true
element_exists?(data, 2) # => false
element_exists?(data, 1, 1) # => true
element_exists?(data, 1, 2) # => false
element_exists?(data, 1, 1, 1) # => false
So, I needed to get some values from associations on my a model:
def provide_list(x,y)
self.send(x.to_sym).map {|k,v| k[y] }
end
Ok, fine. In that model I can take an instance and do
instance.provide_list('resources', 'thefieldiwant')
to be more specific, I'll get an array of the values for the associated resource e.g
accountinstance.provide_list('users', 'numberlogins')
accountinstance.provide_list('users', 'loginproblems')
and get arrays like
[45,56,78,1,2,etc]
[5,5,7,1,2,etc]
and then zip them together, getting tuple arrays ([45,5],[56,6],...) for each user I can pass around and whatnot
I want to improve this, so I don't have to run each time manually when I want a specific array of fields for an association (and as a prelude to doing a method that can get and zip what I need with one call for each association instance):
def provide_listing(dd,*args)
args.each do |ee|
self.send(dd.to_sym).map {|k,v| k[ee] }
end
end
This doesn't work, I just get an array of the args, so something I don't know about is tripping this up. Basic question: how do I get the second to behave in tune with the first, except provide an array for each arg.
Open to suggestions about a better way, what I'm doing wrong, if this has been asked before etc. This seems a basic thing that I can't do right this minute, so I'm asking before putting it away for a while so someone with more expertise can enlighten me.
The solution is me learning more refined methods of manipulating ActiveRecord which I'm hardly expert, reviewing this is helping:
http://guides.rubyonrails.org/active_record_querying.html
the main problem with the second approach is that you are returning the value of #each, which is always just the object #each was called on, in this case args.
I like Pedro's solution, but i'd tweak it slightly:
def provide_list(association, *args)
self.send(association).select(args).map { |r| r.attributes.values }
end
Few things to note:
you don't need to use .to_sym, send and select will take strings or symbols.
i'd avoid using raw sql, sure it will be faster, but more prone to errors, portability issues and sql injection - never write raw sql unless you have a very good reason.
Hope that helps!
Hacky, but should work:
def provide_list association, *args
association = self.send(association.to_sym)
association.connection.select_all("select #{args.join(',')} from #{association.table_name}'").map(&:values)
end
You can try this in the console:
self.send(dd.to_sym)
returns an array of hashes. For example:
#arr = [{a:1, b:2, c:3}, {a:4, b:5, c:6}]
You probably want to return a 2d array like this:
def provide_listing( *args )
args.map do |arg|
#arr.map do |h|
h[arg]
end
end
end
provide_listing( :a, :b, :c )
=> [[1, 4], [2, 5], [3, 6]]
Program I'm making has a simple configuration file looking something like this.
#overlays = {
:foo => "http://www.bar.com",
:bar => nil,
}
What I need to do is go through this hash and get the following output.
OverlayKey[0]='foo'
OverlayVal[0]='http://www.bar.com'
OverlayKey[1]='bar'
OverlayVal[1]='nil'
In order to keep my configuration like I want it I need some fake index numbers. Would rather not add numbers into the hash, it would make the configuration look a bit ugly. So I been playing around with artificially generating the numbers during output.
This is ugly but I"m just playing around with it currently.
def makenumbers
#numbers = []
length = #overlays.length - 1
(0..length).each do |num|
#numbers.push("#{num}")
end
end
makenumbers
#overlays.each do |key,val|
#numbers.each do |num|
puts "OverlayKey['#{num}']='#{key}'"
puts "OverlayVal['#{num}']='#{val}'"
end
end
Which is giving me something like this.
OverlayKey['0']='foo'
OverlayVal['0']='http://www.bar.com'
OverlayKey['1']='foo'
OverlayVal['1']='http://www.bar.com'
OverlayKey['0']='bar'
OverlayVal['0']=''
OverlayKey['1']='bar'
OverlayVal['1']=''
Understand why this doesn't give me the output I want, although after playing with it for a bit I'm not really sure how to do what I want without adding numbers into the hash during configuration. Sure this is pretty simple I just can't seem to wrap my head around it.
I don't know what the problem is other than Hashes are unsorted by default:
overlays = {
:foo => "http://www.bar.com",
:bar => nil,
}
overlays.each_with_index do |(k,v), i|
puts "OverlayKey['#{i}']=#{k.to_s.inspect}"
puts "OverlayVal['#{i}']=#{v.to_s.inspect}"
end
Output looks like this:
OverlayKey['0']="bar"
OverlayVal['0']=""
OverlayKey['1']="foo"
OverlayVal['1']="http://www.bar.com"
As a note:
# Instead of this:
"#{num}"
# Use this:
num.to_s