Count by group and subgroup - ruby-on-rails

I want to generate some stats regarding some data I have in a model
I want to create stats according to an association and a status column.
i.e.
Model.group(:association_id).group(:status).count
to get an outcome like
[{ association_id1 => { status1 => X1, status2 => y1 } },
{ association_id2 => { status1 => x2, status2 => y2 } }...etc
Not really bothered whether it comes out in as an array or hash, just need the numbers to come out consistently.
Is there a 'rails' way to do this or a handy gem?

Ok. Worked out something a little better, though happy to take advice on how to clean this up.
group_counts = Model.group(["association_id","status"]).count
This returns something like:
=> {[nil, "status1"]=>2,
[ass_id1, "status1"]=>58,
[ass_id2, "status7"]=>1,
[ass_id2, "status3"]=>71 ...etc
Which, while it contains the data, is a pig to work with.
stats = group_counts.group_by{|k,v| k[0]}.map{|k,v| {k => v.map{|x| {x[0][1] => x[1] }}.inject(:merge) }}
Gives me something a little friendlier
=> [{
nil => {
"status1" => 10,
"status2" => 23},
"ass_id1" => {
"status1" => 7,
"status2" => 23
}...etc]
Hope that helps somebody.

This is pretty ugly, inefficient and there must be a better way to do it, but...
Model.pluck(:association_id).uniq.map{ |ass| {
name:(ass.blank? ? nil : Association.find(ass).name), data:
Model.where(association_id:ass).group(:status).count
} }
Gives what I need.
Obviously if you didn't need name the first term would be a little simpler.
i.e.
Model.pluck(:association_id).uniq.map{ |ass| {
id:ass, data:Model.where(association_id:ass).group(:status).count
} }

Related

Searching using a single integer in a hash whose keys are ranges in Ruby

I have this hash in Ruby:
hash = {
0..25 => { low_battery_count: 13 },
26..75 => { average_battery_count: 4 },
76..100 => { good_battery_count: 4 }
}
Now, what is want is a method (preferably a built-in one) in which if I pass a value (integer e.g. 20, 30, etc), it should return me the value against the range in which this integer lies.
I have already solved it using each method but now, I want to optimize it,
even more, to do it without each or map methods to reduce its complexity.
For Example:
method(3) #=> {:low_battery_count=>13}
method(35) #=> {:average_battery_count=>4}
method(90) #=> {:good_battery_count=>4}
You can use find for this hash
BATTERIES = {
0..25 => { low_battery_count: 13 },
26..75 => { average_battery_count: 4 },
76..100 => { good_battery_count: 4 }
}
def get_batteries_count(value)
BATTERIES.find { |range, _| range.include?(value) }&.last
end
get_batteries_count(3) #=> {:low_battery_count=>13}
get_batteries_count(35) #=> {:average_battery_count=>4}
get_batteries_count(90) #=> {:good_battery_count=>4}
get_batteries_count(2000) #=> nil
You need to get exact key in order to fetch a value from the hash. So either way a passed number should be tested against the available keys (ranges).
You might consider introducing an optimisation based on assigning pre-determined ranges to the constants and using a case equality check:
SMALL = 0..25
MEDUIM = 26..75
LARGE = 76..100
def method(n)
case n
when SMALL then data[SMALL]
when MEDUIM then data[MEDUIM]
when LARGE then data[LARGE]
end
end
def data
#data ||= {
SMALL => { low_battery_count: 13 },
MEDUIM => { average_battery_count: 4 },
LARGE =>{ good_battery_count: 4 }
}
end
method(25)
=> {:low_battery_count=>13}
method(44)
=> {:average_battery_count=>4}
method(76)
=> {:good_battery_count=>4}
We can do this using some of built in methods but there is not any direct way to do this
class Hash
def value_at_key_range(range_member)
keys.select{ |range| range.include?(range_member) }.map{ |key| self[key]}
end
end
hash = {0..25=>{:low_battery_count=>13}, 26..75=>{:average_battery_count=>4}, 76..100=>{:good_battery_count=>4}}
p hash.value_at_key_range(10)
output
[{:low_battery_count=>13}]

Laravel Model Factory Error: Trying to get property of non-object

I'm trying to use a model factory to seed my database, but when I run it I get the error:
Trying to get property 'id' of non-object
Here is my code:
// TasksTableSeeder.php
factory(pams\Task::class, '2000', rand(1, 30))->create();
// ModelFactory.php
$factory->defineAs(pams\Task::class, '2000', function (Faker\Generator $faker) {
static $task_number = 01;
return [
'task_number' => $task_number++,
'ata_code' => '52-00-00',
'time_estimate' => $faker->randomFloat($nbMaxDecimals = 2, $min = 0.25, $max = 50),
'work_order_id' => '2000',
'description' => $faker->text($maxNbChars = 75),
'action' => '',
'duplicate' => '0',
'certified_by' => '1',
'certified_date' => '2015-11-08',
'status' => '1',
'created_by' => '1',
'modified_by' => '1',
'created_at' => Carbon\Carbon::now()->format('Y-m-d H:i:s'),
'updated_at' => Carbon\Carbon::now()->format('Y-m-d H:i:s'),
];
});
I've tried removing all variables from the model factory and using constants, but that doesn't fix it. I've tried pulling the data from the ModelFactory.php and put it directly into the TasksTableSeeder.php and it does work, however I was using constants and no variables.
I cannot for the life of me figure out what 'id' it's talking about.
I'm running Laravel v5.1
Your BaseModel is not exactly appropriate as it will force you to create hacks in order to run something like a unit test. It will be better that you have a flag in the BaseModel that you test for true or false before setting created_by and modified_by.
Also there's no guarantee there will be user_id '1' at any point in time, unless you actually create that User in your factory first, before creating the Task.
A way to fix your current setup is have a protected field like $enableAuthUpdates which is set to false or true by default. Then you can override the field in any of your derived models such as your Task model to prevent the creating/updating events from running.
Also it's important to make sure your factory has an actual user it is working with and create it if it doesn't exist.
I found the problem. At one stage I had implemented a BaseModel.php model that automatically inserts the current users ID when creating or updating the model. The seeder fails because there isn't a current user, so I had to add a check to see if there was a user logged in first.
Here is the code:
static::creating(function($model)
{
if(Auth::user())
{
$model->created_by = Auth::user()->id;
$model->modified_by = Auth::user()->id;
}
else
{
$model->created_by = '1';
$model->modified_by = '1';
}
});
static::updating(function($model)
{
if(Auth::user())
{
$model->modified_by = Auth::user()->id;
}
else
{
$model->modified_by = '1';
}
});
It's not super pretty, but it gets the job done :)

Convert group count from activerecord into hash with multiple group stages

I'm trying to get some statistics.
Model.where(status:#statuses).group(:sub_group, :status).count
This returns me
{
["sub_group1", "status1"] => 3},
["sub_group2", "status3"] => 7,
["sub_group1", "status2"] => 5, ....etc }
I want to merge them so each element has a unique subgroup.
e.g. a hash like:
{
"sub_group1" => {"status1" => 3, "status2" => 5,
"sub_group2" => {"status3" => 7},
}
or an array
[
["subgroup1", {"status1" = 3, "status2" => 5}],
["subgroup2".....
]
i.e. I want all those terms to be merged with sub_group as the primary header so that I can get the results for subgroup in one item.
Brain not working today....
You can try with merge!:
result = Model.where(status: #statuses).group(:sub_group, :status).count
result.reduce({}) do |collection, (attributes, count)|
collection.merge!(attributes[0] => { attributes[1] => count }) do |_, prev_value, next_value|
prev_value.merge!(next_value)
end
end
Demonstration
I suppose you could do something like this:
res = Model.where(status:#statuses).group(:sub_group, :status).count
nice_hash = {}
res.each do |key, count|
nice_hash[key[0]] = {} unless nice_hash[key[0]]
nice_hash[key[0]][key] = count
end
After this nice_hash should be on the desired format
Please try with below code.
a = Model.where(status:#statuses).group(:sub_group, :status).count
res = {}
a.each do |k,v|
c={}
if res.has_key?(k[0])
c[k[1]]=v
res[k[0]]=res[k[0]].merge(c)
else
c[k[1]]=v
res[k[0]]=c
end
end
I believe the answer to your question should be this or this should guide in the right direction:
Model.where(status:#statuses).group_by(&:sub_group)
If you need the only the statuses as you mentioned, you could do:
Model.where(status:#statuses).select(:status, :sub_group).group_by(&:sub_group)

Ruby Mongo Driver Projection Elemmatch

Following the code in http://www.w3resource.com/mongodb/mongodb-elemmatch-projection-operators.php I have set up a test database using the ruby mongodb driver.
For those following along at home, you first need to install the mongo driver as described at https://docs.mongodb.com/ecosystem/tutorial/ruby-driver-tutorial/#creating-a-client, then run the following commands.
client = Mongo::Client.new([ '127.0.0.1:27017'], :database => 'mydb')
test = client['test']
doc = {
"_id" => 1,
"batch" =>10452,
"tran_details" =>[
{
"qty" =>200,
"prate" =>50,
"mrp" =>70
},
{
"qty" =>250,
"prate" =>50,
"mrp" =>60
},
{
"qty" =>190,
"prate" =>55,
"mrp" =>75
}
]
}
test.insert_one(doc)
Insert all of the different docs created in the w3 tutorial.
If you look at example 2 in the w3 tutorial, the translated ruby find is:
test.find({"batch" => 10452}).projection({"tran_details" => {"$elemMatch" => {"prate" => 50, "mrp" => {"$gte" => 70}}}}).to_a
which returns the same result as in the example.
=> [{"_id"=>1, "tran_details"=>[{"qty"=>200, "prate"=>50, "mrp"=>70}]}, {"_id"=>3}, {"_id"=>4}]
My problem is that I would like to constrain the results with the constraints above (mrp gte 70 etc) while also specifying which fields are returned.
For instance, constraining only the tran_details that have a mrp gte 70, but in the results returned only include the prate field (or any subset of the fields).
I can return only the prate field with the query:
test.find({"batch" => 10452}).projection({"tran_details.prate" => 1}).to_a
I would like to combine the effects of the two different projections, but I haven't seen any documentation about how to do that online. If you string the two projections to each other, only the final projection has an effect.
To anyone out there --
The problem can be solved up to one element by using $elemMatch on projection. However, $elemMatch only returns the first result found. To return only parts of embedded documents multiple layers down that fit certain criteria, you need to use the aggregation framework.
test.find({
'tran_details.prate' => { '$gt' => 56 }
}).projection({
tran_details: {
'$elemMatch' => {
prate: { '$gt' => 56 }
}
},
'tran_details.qty' => 1,
'tran_details.mrp' => 1,
batch: 1,
_id: 0
}).to_a
To return only parts of embedded documents multiple layers down that fit certain criteria, you need to use the aggregation framework.
Here is example code
test.aggregate([{
'$match': {
'$or': [
{'batch': 10452}, {'batch': 73292}]}},
{'$project':
{trans_details:
{'$filter': {
input: '$tran_details',
as: 'item',
cond: {'$and':[
{'$gte' => ['$$item.prate', 51]},
{'gt' => ['$$item.prate', 53]}
]}
}
}
}
}]).to_a
If anyone sees this and knows how to dynamically construct queries in ruby from strings please let me know! Something to do with bson but still trying to find relevant documentation. Thanks -

Ruby way to loop and check subsequent values against each other

I have an array that contains dates and values. An example of how it might look:
[
{'1/1/2010' => 'aa'},
{'1/1/2010' => 'bb'},
{'1/2/2010' => 'cc'},
{'1/2/2010' => 'dd'},
{'1/3/2010' => 'ee'}
]
Notice that some of the dates repeat. I'm trying to output this in a table format and I only want to show unique dates. So I loop through it with the following code to get my desired output.
prev_date = nil
#reading_schedule.reading_plans.each do |plan|
use_date = nil
if plan.assigned_date != prev_date
use_date = plan.assigned_date
end
prev_date = plan.assigned_date
plan.assigned_date = use_date
end
The resulting table will then look something like this
1/1/2010 aa
bb
1/2/2010 cc
dd
1/3/2010 ee
This work fine but I am new to ruby and was wondering if there was a better way to do this.
Enumerable.group_by is a good starting point:
require 'pp'
asdf = [
{'1/1/2010' => 'aa'},
{'1/1/2010' => 'bb'},
{'1/2/2010' => 'cc'},
{'1/2/2010' => 'dd'},
{'1/3/2010' => 'ee'}
]
pp asdf.group_by { |n| n.keys.first }.map{ |a,b| { a => b.map { |c| c.to_a.last.last } } }
# >> [{"1/1/2010"=>["aa", "bb"]}, {"1/2/2010"=>["cc", "dd"]}, {"1/3/2010"=>["ee"]}]
Which should be a data structure you can bend to your will.
I don't know as though it's better, but you could group the values by date using (e.g.) Enumerable#reduce (requires Ruby >= 1.8.7; before that, you have Enumerable#inject).
arr.reduce({}) { |memo, obj|
obj.each_pair { |key, value|
memo[key] = [] if ! memo.has_key?(key);
memo[key] << value
}
memo
}.sort
=> [["1/1/2010", ["aa", "bb"]], ["1/2/2010", ["cc", "dd"]], ["1/3/2010", ["ee"]]]
You could also use Array#each to similar effect.
This is totally a job for a hash.
Create a hash and use the date as the hashkey and an empty array as the hashvalue.
Then accumulate the values from the original array in the hashvalue array

Resources