I would like sort array of ActiveRecord objects by related object's attribute value. Meaning something like this:
Item has one product which has an attribute SKU. The SKU is mostly integer stored as a string, but could be alphanumeric as well.
sorted = items.sort_by { |item| Integer(item.product.sku) } rescue items
For now in case of error the items with original order returns.
What would I like to do?
Extend the Array class to achieve something like:
items.numeric_sort { |item| item.product.sku }
What I did so far?
1. Building a lambda expression and passing it
class Array
def numeric_sort(&lambda)
if lambda.respond_to? :call
self.sort_by(&lambda) rescue self
else
self.sort_by { |el| Integer(el) } rescue self
end
end
end
product_bin = lambda { |task_item| Integer(item.product.bin) }
items.numeric_sort(&product_bin)
2. Building lambda expression from methods chain
class Object
def send_chain(keys)
keys.inject(self, :send)
end
end
class Array
def numeric_sort_by(*args)
(args.length == 1) ? lam = lambda {|el| Integer(el.send(args))} : lam = lambda {|el| Integer(el.send_chain(args))}
self.sort_by(&lam) rescue self
end
end
items.numeric_sort_by(:product, :sku)
Is it all makes any sense?
Can you please point me in the right direction to implement the syntax I mentioned above, if it is possible at all.
Thanks.
EDIT: the sku could be alphanumeric as well. Sorry for the confusion.
Try this solution.
There is no error handling.
It's just an idea to develop if you like it.
class Array
def numeric_sort_by(*args)
self.sort_by do |element|
object = element
args.size.times { |n| object = object.send(args[n]) }
object.to_f
end
end
end
items.numeric_sort_by 'product', 'sku'
So the straightforward implementation was:
sorted = items.sort_by { |item| Integer(item.product.sku) } rescue items
And the desired was:
items.numeric_sort_by { |item| item.product.sku }
I was manage to achieve it by yielding a block into the sort_by:
class Array
def numeric_sort_by(&block)
return to_enum :numeric_sort_by unless block_given?
self.sort_by { |element| Integer(yield(element)) } rescue self
end
end
Related
I've got a class that looks like this that turns a collection into a nested array of hashes:
# variable_stack.rb
class VariableStack
def initialize(document)
#document = document
end
def to_a
#document.template.stacks.map { |stack| stack_hash(stack) }
end
private
def stack_hash(stack)
{}.tap do |hash|
hash['stack_name'] = stack.name.downcase.parameterize.underscore
hash['direction'] = stack.direction
hash['boxes'] = stack.boxes.indexed.map do |box|
box_hash(box)
end.reverse_if(stack.direction == 'up') # array extensions
end.delete_if_key_blank(:boxes) # hash extensions
end
def box_hash(box)
{}.tap do |hash|
hash['box'] = box.name.downcase.parameterize.underscore
hash['content'] = box.template_variables.indexed.map do |var|
content_array(var)
end.join_if_any?
end.delete_if_key_blank(:content)
end
def content_array(var)
v = #document.template_variables.where(master_id: var.id).first
return unless v
if v.text.present?
v.text
elsif v.photo_id.present?
v.image.uploaded_image.url
else
''
end
end
end
# array_extensions.rb
class Array
def join_if_any?
join("\n") if size.positive?
end
def reverse_if(boolean)
reverse! if boolean
end
end
# hash_extensions.rb
class Hash
def delete_if_key_blank(key)
delete_if { |_, _| key.to_s.blank? }
end
end
This method is supposed to return a hash that looks like this:
"stacks": [
{
"stack_name": "stack1",
"direction": "down",
"boxes": [
{
"box": "user_information",
"content": "This is my name.\n\nThis is my phone."
}
},
{
"stack_name": "stack2",
"direction": "up",
"boxes": [
{
"box": "fine_print",
"content": "This is a test.\n\nYeah yeah."
}
]
}
Instead, often the boxes key is null:
"stacks": [
{
"stack_name": "stack1",
"direction": "down",
"boxes": null
},
{
"stack_name": "stack2",
"direction": "up",
"boxes": [
{
"box": "fine_print",
"content": "This is a test.\n\nYeah yeah."
}
]
}
I suspect it's because I can't "single-line" adding to arrays in Rails 5 (i.e., they're frozen). The #document.template.stacks is an ActiveRecord collection.
Why can't I map records in those collections into hashes and add them to arrays like hash['boxes']?
The failing test
APIDocumentV3 Instance methods #stacks has the correct content joined and indexed
Failure/Error:
expect(subject.stacks.first['boxes'].first['content'])
.to include(document.template_variables.first.text)
expected "\n" to include "#1"
Diff:
## -1,2 +1 ##
-#1
The presence of \n means the join method works, but it shouldn't join if the array is empty. What am I missing?
reverse_if returns nil if the condition is false. Consider this:
[] if false #=> nil
You could change it like this:
def reverse_if(condition)
condition ? reverse : self
end
delete_if_key_blank doesn't look good for me. It never deletes anything.
Disclaimer. I don't think it's a good idea to extend standard library.
So thanks to Danil Speransky I solved this issue, although what he wrote doesn't quite cover it.
There were a couple of things going on here and I solved the nil arrays with this code:
hash['boxes'] = stack.boxes.indexed.map do |box|
box_hash(box) unless box_hash(box)['content'].blank?
end.reverse_if(stack.direction == 'up').delete_if_blank?
end
That said, I'm almost certain my .delete_if_blank? extension to the Array class isn't helping at all. It looks like this, FYI:
class Array
def delete_if_blank?
delete_if(&:blank?)
end
end
I solved it by thowing the unless box_hash(box)['content'].blank? condition on the method call. It ain't pretty but it works.
I want to convert all the values in a nested hash to a utf8 compatible string. I initially thought this would be easy and something like deep_apply should be available for me to use, but I am unable to find anything this simple on a quick google and SO search.
I do not want to write (maintain) a method similar to the lines of Change values in a nested hash . Is there a native API implementation or a shorthand available for this or do I have to write my own method?
I ended up implementing my own approach, that is in no way perfect but works well for my use case and should be easy to maintain. Posting it here for reference to anyone who wants to try it out
def deep_apply object, klasses, &blk
if object.is_a? Array
object.map { |obj_ele| deep_apply(obj_ele, klasses, &blk) }
elsif object.is_a? Hash
object.update(object) {|_, value| deep_apply(value, klasses, &blk) }
elsif klasses.any? { |klass| object.is_a? klass }
blk.call(object)
else
object
end
end
usage:
=> pry(main)> deep_apply({a: [1, 2, "sadsad"]}, [String, Integer]) { |v| v.to_s + "asd" }
=> {:a=>["1asd", "2asd", "sadsadasd"]}
Interesting to learn of the deep_merge approach taken in the answer by "The F". Here is another approach which requires adding a few helper methods.
First, the helper methods:
From the top answer here (converting-a-nested-hash-into-a-flat-hash):
def flat_hash(h,f=[],g={})
return g.update({ f=>h }) unless h.is_a? Hash
h.each { |k,r| flat_hash(r,f+[k],g) }
g
end
From a Github repo called ruby-bury (this functionality was proposed to Ruby core, but rejected)
class Hash
def bury *args
if args.count < 2
raise ArgumentError.new("2 or more arguments required")
elsif args.count == 2
self[args[0]] = args[1]
else
arg = args.shift
self[arg] = {} unless self[arg]
self[arg].bury(*args) unless args.empty?
end
self
end
end
And then a method tying it together:
def change_all_values(hash, &blk)
# the next line makes the method "pure functional"
# but can be removed otherwise.
hash = Marshal.load(Marshal.dump(hash))
flat_hash(hash).each { |k,v| hash.bury(*(k + [blk.call(v)])) }
hash
end
A usage example:
irb(main):063:0> a = {a: 1, b: { c: 1 } }
=> {:a=>1, :b=>{:c=>1}}
irb(main):064:0> b = change_all_values(a) { |val| val + 1 }
=> {:a=>2, :b=>{:c=>2}}
irb(main):066:0> a
=> {:a=>1, :b=>{:c=>1}}
There is deep_merge
yourhash.deep_merge(yourhash) {|_,_,v| v.to_s}
Merge the hash with itself, inspect the value and call to_s on it.
This method requires require 'active_support/core_ext/hash' at the top of file if you are not using ruby on rails.
Obviously, you may handle the conversion of v inside the deep_merge as you like to meet your requirements.
In rails console:
2.3.0 :001 > h1 = { a: true, b: { c: [1, 2, 3] } }
=> {:a=>true, :b=>{:c=>[1, 2, 3]}}
2.3.0 :002 > h1.deep_merge(h1) { |_,_,v| v.to_s}
=> {:a=>"true", :b=>{:c=>"[1, 2, 3]"}}
Well, it's quite simple to write it - so why don't write your own and be absolutely sure how does it behave in all situations ;)
def to_utf8(h)
if h.is_a? String
return h.force_encoding('utf-8')
elsif h.is_a? Symbol
return h.to_s.force_encoding('utf-8').to_sym
elsif h.is_a? Numeric
return h
elsif h.is_a? Array
return h.map { |e| to_utf8(e) }.to_s
else
return h.to_s.force_encoding('utf-8')
end
return hash.to_a.map { |e| result.push(to_utf8(e[0], e[1])) }.to_h
end
You may want to check if all behavior and conversions are correct - and change it if necessary.
I have a Product model which has many Items. The application lists unique items which belong to a product. So think of items as inventory. The following query grabs featured items for a product and removes the first item (irrelevant, but it becomes a featured item, displayed separately, if you're curious).
# product.rb
has_many :items_in_stock, -> { Item.in_stock }, class_name: 'Item'
def featured_items
items_in_stock.select("DISTINCT ON (condition) id, items.*")
.order(:condition, :price)
.sort_by { |item| item[:price] }[1..-1]
end
# item.rb
scope :in_stock, -> { where(status: 'in_stock') }
The trouble is when the feaured_items are empty, the method returns nil, and not a relation object. This means I get an error if I call #product.featured_items.any? on a product that has no items. If I remove the sort_by block, I get an empty relation object.
Is there a good way to handle this other than:
items = items_in_stock.select("DISTINCT ON (condition) id, items.*").order(:condition, :price)
if items.any?
items.sort_by { |item| item[:price] }[1..-1]
end
I can't reverse the ordering of the query because I get an error saying the order of the conditions in the order by statement must match the group conditions.
I'm confused...why call .any? on it then since nil is treated as false in ruby. If what you get back is nil then you know that you don't have any featured_items.
I ran this in irb and I think your issue is the [1..-1].
a = []
# => []
a.sort_by { |w| w.length }
# => []
a.sort_by { |w| w.length }[1..-1]
# => nil
The easiest way is to just do
items = items_in_stock.select("DISTINCT ON (condition) id, items.*")
.order(:condition, :price)
.sort_by { |item| item[:price] }
items.any? ? items[1..-1] : items
Then you don't actually have to do a check in other parts of your code unless it's necessary.
instead of if items.any? you can use unless items.blank? if it's nil or empty, it won't run the condition
items.blank? checks both items.empty? and items.nil?
And of course you can use it in your featured_items
items = items_in_stock.select("DISTINCT ON (condition) id, items.*")
.order(:condition, :price)
.sort_by { |item| item[:price] }[1..-1]
return Array.new if items.blank?
That way you know that result will be an array, no matter what
And for the proof, you can use .blank? on a nil object, and it works on nil itself, nil.blank? returns true
I have this in models_name:
model_names = Object.constants.collect { |sym| Object.const_get(sym) }.select { |constant| constant.class == Class && constant.include?(Mongoid::Document) }.collect { |klass| klass.name }
the result is:
["Model_name1","Model_name2","Model_name3"]
I need something like:
[{"Model1":"Count"},{"Model2":"Count"},{"Model3":"Count"}]
I need show in a chart all models and object counter inside each model is working with morris.js
You can see the example:
http://jsbin.com/uzosiq/2/embed?javascript,live
Thank you very much!
that last one:
collect { |klass| klass.name }
rewrite it as
collect { |klass| [klass.name, klass.count] }
this should return an array of arrays of 2 elements (classname and count). If the variable to which this is assigned is called a, just do this:
Hash[a]
now you have a hash at your disposal. Now you can do this:
Hash[a].map do |k, v|
{k => v}
end
and now you have an array of hashes of only one key-value assoc each. Which I think it is what you want.
module Foo
end
class Bar
include Foo
end
1.upto(5) { Bar.new }
model_names = Object.constants.collect { |sym| Object.const_get(sym) }.
select { |constant| constant.class == Class && constant.include?(Foo) }.
inject({}) do |m,klass|
m.update klass.name => ObjectSpace.each_object(klass).to_a.size
end
p model_names
{"Bar"=>5}
Will work only on MRI based rubies, so no jRuby nor Runbinius.
Here is a live demo
I am using ruby-aaws to return Amazon Products and I want to enter them into my DB. I have created a model Amazonproduct and I have created a method get_amazon_data to return an array with all the product information. When i define the specific element in the array ( e.g. to_a[0] ) and then use ruby-aaws item_attributes method, it returns the name I am searching for and saves it to my DB. I am trying to iterate through the array and still have the item_attributes method work. When i don't define the element, i get this error: undefined method `item_attributes' for #Array:0x7f012cae2d68
Here is the code in my controller.
def create
#arr = Amazonproduct.get_amazon_data( :r ).to_a
#arr.each { |name|
#amazonproduct = Amazonproduct.new(params[:amazonproducts])
#amazonproduct.name = #arr.item_attributes.title.to_s
}
EDIT: Code in my model to see if that helps:
class Amazonproduct < ActiveRecord::Base
def self.get_amazon_data(r)
resp = Amazon::AWS.item_search('GourmetFood', { 'Keywords' => 'Coffee Maker' })
items = resp.item_search_response.items.item
end
end
Thanks for any help/advice.
I'm not familiar with the Amazon API, but I do observe that #arr is an array. Arrays do not usually have methods like item_attributes, so you probably lost track of which object was which somewhere in the coding process. It happens ;)
Try moving that .item_attributes call onto the object that supports that method. Maybe amazonproduct.get_amazon_data(:r), before its being turned into an array with to_a, has that method?
It's not quite clear to me what your classes are doing but to use #each, you can do something like
hash = {}
[['name', 'Macbook'], ['price', 1000]].each do |sub_array|
hash[sub_array[0]] = sub_array[1]
end
which gives you a hash like
{ 'name' => 'Macbook', 'price' => 1000 }
This hash may be easier to work with
#product = Product.new
#product.name = hash[:name]
....
EDIT
Try
def create
#arr = Amazonproduct.get_amazon_data( :r ).to_a
#arr.each do |aws_object|
#amazonproduct = Amazonproduct.new(params[:amazonproducts])
#amazonproduct.name = aws_object.item_attributes.title.to_s
end
end