Filter data in JSON string - ruby-on-rails

I have a JSON string as pulled from some API
[{"_id"=>"56aefb3b653762000b400000",
"checkout_started_at"=>"2016-02-01T07:32:09.120+01:00",
"created_at"=>"2016-02-01T07:29:15.695+01:00", ...}]
I want to filter data in this string based on created_at, e.g. letting the user chose a specific date-range and then only show the data from this range.
E.g.
#output = my_json.where(created_at: start_date..end_date)
My first thought was to somehow transfer the JSON string to Hashie, to interact with JSON as the data were objects:
my_json = (my_json).map { |hash| Hashie::Mash.new(hash) }
but that didn't work out
undefined method `where' for Array:0x007fd0bdeb84e0
How can I filter out data from a JSON string based on specific criteria or even SQL queries?

This simplest possible way would be to use Enumberable#select directly on the array of hashes:
require 'time'
myjson.select { |o| Time.iso8601(o["created_at"]).between?(start_date, end_date) }
If you want a fancy interface surrounding the raw data:
require 'time' # not needed in rails
class Order < Hashie::Mash
include Hashie::Extensions::Coercion
coerce_key :created_at, ->(v) do
return Time.iso8601(v)
end
end
class OrderCollection
def initialize(json)
#storage = json.map { |j| Order.new(j) }
end
def between(min, max)
#storage.select { |t| t.created_at.between?(min, max) }
end
end
orders = OrderCollection.new(myjson)
orders.between(Time.yesterday, Time.now)

Related

Too many checks for empty params. How to optimize queries to ActiveRecord in Rails5?

I'm doing checks for empty parameters before do the query.
There is only 1 check for params[:car_model_id]. I can imagine if I will add more checks for other params, then there will be a mess of if-else statements. It doesn't look nice and I think it can be optimized. But how? Here is the code of controller:
class CarsController < ApplicationController
def search
if params[:car_model_id].empty?
#cars = Car.where(
used: ActiveRecord::Type::Boolean.new.cast(params[:used]),
year: params[:year_from]..params[:year_to],
price: params[:price_from]..params[:price_to],
condition: params[:condition]
)
else
#cars = Car.where(
used: ActiveRecord::Type::Boolean.new.cast(params[:used]),
car_model_id: params[:car_model_id],
year: params[:year_from]..params[:year_to],
price: params[:price_from]..params[:price_to],
condition: params[:condition]
)
end
if #cars
render json: #cars
else
render json: #cars.errors, status: :unprocessable_entity
end
end
end
The trick would be to remove the blank values, do a little bit of pre-processing (and possibly validation) of the data, and then pass the params to the where clause.
To help with the processing of the date ranges, you can create a method that checks both dates are provided and are converted to a range:
def convert_to_range(start_date, end_date)
if start_date && end_date
price_from = Date.parse(price_from)
price_to = Date.parse(price_to)
price_from..price_to
end
rescue ArgumentError => e
# If you're code reaches here then the user has invalid date and you
# need to work out how to handle this.
end
Then your controller action could look something like this:
# select only the params that are need
car_params = params.slice(:car_model_id, :used, :year_from, :year_to, :price_from, :price_to, :condition)
# do some processing of the data
year_from = car_params.delete(:year_from).presence
year_to = car_params.delete(:year_to).presence
car_params[:price] = convert_to_range(year_from, year_to)
price_from = car_params.delete(:price_from).presence
price_to = car_params.delete(:price_to).presence
car_params[:price] = convert_to_range(price_from, price_to)
# select only params that are present
car_params = car_params.select {|k, v| v.present? }
# search for the cars
#cars = Car.where(car_params)
Also, I'm pretty sure that the used value will automatically get cast to boolean for you when its provided to the where.
Also, #cars is an ActiveRecord::Relation which does not have an errors method. Perhaps you mean to give different results based on whether there are any cars returned?
E.g: #cars.any? (or #cars.load.any? if you don't want to execute two queries to fetch the cars and check if cars exist)
Edit:
As mentioned by mu is too short you can also clean up your code by chaining where conditions and scopes. Scopes help to move functionality out of the controller and into the model which increases re-usability of functionality.
E.g.
class Car > ActiveRecord::Base
scope :year_between, ->(from, to) { where(year: from..to) }
scope :price_between, ->(from, to) { where(price: from..to) }
scope :used, ->(value = true) { where(used: used) }
end
Then in your controller:
# initial condition is all cars
cars = Cars.all
# refine results with params provided by user
cars = cars.where(car_model_id: params[:car_model_id]) if params[:car_model_id].present?
cars = cars.year_between(params[:year_from], params[:year_to])
cars = cars.price_between(params[:price_from], params[:price_to])
cars = cars.used(params[:used])
cars = cars.where(condition: params[:condition]) if params[:condition].present?
#cars = cars

How can I pass a hash for my where clause?

I am listing products and I want to be able to pass a hash as my where clause so I can do something like:
filter = {}
filter[:category_id] = #category.id
filter[:is_active] = true
#products = Products.where(filter)
Is it possible to do this somehow?
I also need to add something like this in my where clause:
WHERE price > 100
How could I add that to a filter?
The reason I want to do this is because in the UI I will have a set of optional filters, so then I will use if clauses in my controller to set each filter.
You can pass a hash to where exactly like you did:
filter = {
category_id: #category_id,
is_active: true
}
#products = Product.where(filter)
Using a hash only works for equality (e.g. category_id = 123), so you can't put something like price > 100 in there. To add that criteria, just add another where to the chain:
#product = Product.where(filter).where('price > 100')
Or...
#product = Product.where(filter)
if params[:min_price]
#product = #product.where('price > ?', min_price)
end
You could have a bit of fun with scopes: write a scope that's actually a mini predicate builder, sanitizing and pattern-matching strings, and delegating to the standard predicate builder for other scalar types. E.g.
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(params) {
params.inject(self) do |rel, (key, value)|
next rel if value.blank?
case value
when String
rel.where arel_table[key].matches '%%%s%%' % sanitize_sql_like(value)
when Range, Numeric, TrueClass, FalseClass
rel.where key => value
else
raise ArgumentError, "unacceptable search type"
end
end
}
end
end
# app/models/product.rb
class Product < ApplicationRecord
include Searchable
then you can
filter = { name: 'cheese', description: 'aged', age: 42.. }
Product.search(filter) #=> SELECT "products".* FROM products WHERE "products"."name" ILIKE '%cheese%' AND "products"."description" ILIKE '%aged%' AND "products"."age" >= 42

Activerecord Group by ENUM

I'm using rails 4.1 and the new enum functionality to include an array of enums in my model class e.g:
class Campaign < ActiveRecord::Base
enum status: [:pending, :active, :paused, :complete]
end
I want to query campaigns and list a count by status e.g:
Campaign.all.group("status").count
This simple query works fine however returns the integer value of the enum from the DB. Is there an easy rails way to convert this to the string representation?
Just map numbers to related string values:
Campaign.all.group(:status).count.map { |k, v| [Campaign.statuses.key(k), v] }.to_h
if you want status wise count
campaigns = Campaign.all.group(:status).count
#campaigns = Campaign.statuses.map { |k, v| [k , campaigns[v] || 0] }
it will give output like
{"pending"=>0, "active"=>1, "paused"=>0, "complete"=>0}
Also you can use rails #transform_keys method:
Campaign.all.group(:status).count.transform_keys { |k| Campaign.statuses.key(k) }

Handle bad JSON gracefully with Hash#fetch

I'm trying to fetch some products from this JSON API so I can display them in my views, with bad JSON gracefully handled using Hash#fetch to declare a default value if I get nil.
But why am I getting:
can't convert String into Integer
or if I set #json_text to {}:
undefined method 'fetch' for `nil:NilClass`
Live app: http://runnable.com/U-QEWAnEtDZTdI_g/gracefully-handle-bad-json
class MainController < ApplicationController
require 'hashie'
def index
#json_text = <<END
{
"products": []
}
END
hashes = JSON.parse(#json_text)
mashes = Hashie::Mash.new(hashes)
products = []
mashes.products.fetch('products', []).each do |mash|
puts "YOLO"
end
end
end
The right way to fetch is by calling fetch on mashes variable:
mashes.fetch('products', []).each do |mash|
puts "YOLO"
end
#fetch is a Hash method and in this case is the same as mashes['product'], except when product is nil, using the fetch example will return []

Sort Array By Date Attribute, Ordered By Nearest to Current Time

I have an Array of Contact objects...
[#<Contact id: 371, full_name: "Don Hofton", birthday: "2013-11-07">,...]
And I need to order them by birthdays nearest to the current time AND remove objects from the array that have birthdays greater than 4 months away. Here is what I've got so far, but it's not working....
#contacts_with_birthday_data = Contact.where(:user_id => current_user.id).where("birthday IS NOT NULL")
#current_time = Time.now
#contacts_with_birthday_data.each do |c|
c.birthday = c.birthday[0..4]
c.birthday = Date.parse(c.birthday)
end
#contacts_with_birthday_data = #contacts_with_birthday_data.sort! { |a,b| b[:birthday] <=> a[:birthday] }
#contacts_with_birthday_data = #contacts_with_birthday_data.sort! { |a| a.birthday < DateTime.now }
I think you can do this all with one query:
Contact \
.where(:user_id => current_user.id)
.where("birthday > ?", 4.months.ago)
.order("birthday desc")
If 4.months.ago is used in a scope, make sure to wrap it in a lambda or Proc, or it will be calculated when the class is loaded and not on subsequent calls. This has bit me more than once!
Alternatively, in a non-Rails world, you could use the reject and sort_by methods on Enumerable:
contacts = [#your array of contacts]
contacts.reject { |c| c.birthday < 4.months.ago }.sort_by(&:birthday).reverse
If you haven't seen the syntax used in sort_by, that's actually equivalent to sort_by { |c| c.birthday }. That syntax tells Ruby to convert the birthday method to a Proc object, then call the Proc against each instance in your array.

Resources