Rails group a record by only its first digit - ruby-on-rails

I have the following stuff in my DB:
postcode
66
2
34
25
54
98
1
14
5
39
37
Now what is a nice way to group this records by only the first digit (and count it)?
So the hash output is sth. like this:
1 => 2
5 => 2
9 => 1
3 => 3
etc
Model.group(:postcode).count
would be the standard query here to consider both digits. Is there any addition needed?
Or should I straight modify the DB and cut the 2nd digit (I don't want that).

Get all the postcode from db as an Array
array = Model.pluck(:postcode)
Group by first digit, map values array to get count
Hash[ array.group_by {|n| n.to_s[0] }.map {|k, v| [k.to_i, v.size] }]

grouped_by = ['65', '15', '11', '29','4','56'].group_by { |n| n[0] }
# {"6"=>["65"], "1"=>["15", "11"], "2"=>["29"], "4"=>["4"], "5"=>["56"]}
grouped_by.map{ |k,v| {k => v.count} }
# [{"6"=>1}, {"1"=>2}, {"2"=>1}, {"4"=>1}, {"5"=>1}]
It first groups by the first digit and then maps over the resulting hash and replaces the values with the count of the values.

For a pure SQL implementation:
Model.group("SUBSTR(CAST(postcode as CHAR), 1, 1)").count

Related

How to sort collection_select drop-down menu which consists of strings with numbers?

I'm building a Rails (4.1.8) / Ruby (2.1.5) application. I have the following collection_select form field:
<%= f.collection_select :target_ids, Target.order(:outcome), :id, :outcome, { :prompt => "-- Select one to two --" }, { :multiple => true, :size => 6 } %>
This works as expected, but I need help sorting the order of items in the drop-down menu, which consists of strings with numbers. My current code sorts the list in ascending order by letter but the numbers in the strings are out of order. Here's a snippet to show what the menu looks like now:
-- Select one to two --
Gain 10 pounds
Gain 15 pounds
Gain 1 pound
Lose 10 pounds
Lose 15 pounds
Lose 1 pound
Reduce body fat by 15%
Reduce body fat by 2%
Here's how I want to sort the menu:
-- Select one to two --
Gain 1 pound
Gain 10 pounds
Gain 15 pounds
Lose 1 pound
Lose 10 pounds
Lose 15 pounds
Reduce body fat by 2%
Reduce body fat by 15%
How do I get the numbers in the strings to display in the order I want? Is there a Ruby sort method that handles this kind of sort out-of-the -box, or do I need to write a custom sort method? How do I best achieve the result I'm looking for? Thank you!
TL;DR
Use Enumerable#sort_by with a block that extracts the leading text and the digits (converted to Fixnums) from each outcome:
class Target < ActiveRecord::Base
OUTCOME_PARTS_EXPR = /(\D+)(\d+)/
# ...
def self.sort_by_outcome
all.sort_by do |target|
outcome = target.outcome
next [ outcome ] unless outcome =~ OUTCOME_PARTS_EXPR
[ $1, $2.to_i ]
end
end
end
Then, in your controller:
#targets_sorted = Target.sort_by_outcome
And, finally, in your view:
<%= f.collection_select :target_ids, #targets_sorted, :id, :outcome,
{ :prompt => "-- Select one to two --" }, { :multiple => true, :size => 6 } %>
How it works
In general you should try to do all of your sorting in the database, but since I don't know what database you're using I'm going to limit my answer to how to do this in Ruby. With relatively few records the performance penalty will be negligible anyway—premature optimization and all that.
In Ruby the Enumerable#sort_by method will sort an Enumerable by the result of a block. It looks like you want to sort your values first by the part before the digits (e.g. "Gain ", "Reduce body fat by ") and then by the integer value of the digits (e.g. the Fixnum 10 instead of the string "10"). Let's break it into steps:
1. Get the desired parts from the string.
This is easy with a regular expression:
OUTCOME_PARTS_EXPR = /(\D+)(\d+)/
str = "Gain 10 pounds"
str.match(OUTCOME_PARTS_EXPR).captures
# => [ "Gain ", "10" ]
Note: This regular expression assumes that there will always be some non-digit text before the digits. If that's not the case this may need some modification.
2. Do it to every element in an Enumerable
Using Enumerable#map we can do it for a whole Enumerable, and while we're at it we can convert the digits to Fixnums (e.g. "10" to 10):
arr = [
"Gain 10 pounds",
"Gain 15 pounds",
"Gain 1 pound",
"Lose 10 pounds",
"Lose 15 pounds",
"Lose 1 pound",
"Reduce body fat by 15%",
"Reduce body fat by 2%"
]
arr.map do |str|
next [ str ] unless str =~ OUTCOME_PARTS_EXPR
[ $1, $2.to_i ]
end
The block we're passing to map does two things: If the string doesn't match our regular expression it just returns the string (as a single-element array). Otherwise, it returns an array with the first capture ("Gain ") as a string and the second capture (10) as a Fixnum. This is the result:
[ [ "Gain ", 10 ],
[ "Gain ", 15 ],
[ "Gain ", 1 ],
[ "Lose ", 10 ],
[ "Lose ", 15 ],
[ "Lose ", 1 ],
[ "Reduce body fat by ", 15 ],
[ "Reduce body fat by ", 2 ] ]
3. Sort by those parts
Now that we know how to get the parts we need, we can pass the same block to sort_by to sort by them:
arr.sort_by do |str|
next [ str ] unless str =~ OUTCOME_PARTS_EXPR
[ $1, $2.to_i ]
end
# => [ "Gain 1 pound",
# "Gain 10 pounds",
# "Gain 15 pounds",
# "Lose 1 pound",
# "Lose 10 pounds",
# "Lose 15 pounds",
# "Reduce body fat by 2%",
# "Reduce body fat by 15%" ]
Great! But there's one last step...
4. Do it to a collection returned by an ActiveRecord query
We're not sorting an array of strings, we're sorting a collection of Target objects. This isn't much of a complication, though; we just need to point to the right attribute, i.e. outcome:
Target.all.sort_by do |target|
outcome = target.outcome
next [ outcome ] unless outcome =~ OUTCOME_PARTS_EXPR
[ $1, $2.to_i ]
end
Note that I changed Target.order(:outcome) to Target.all. Since we're sorting in Ruby it probably doesn't make sense to also sort in the database query.
5. All together now
To clean things up, you should probably put this in your Target model, e.g.:
class Target < ActiveRecord::Base
OUTCOME_PARTS_EXPR = /(\D+)(\d+)/
# ...
def self.sort_by_outcome
all.sort_by do |target|
outcome = target.outcome
next [ outcome ] unless outcome =~ OUTCOME_PARTS_EXPR
[ $1, $2.to_i ]
end
end
end
As a class method you can call this like Target.sort_by_outcome, or you can put it on the end of a query like Target.where(...).sort_by_outcome. So in your case you'd want to put this in the controller (as a rule of thumb you should never do ActiveRecord queries in your view):
#targets_sorted = Target.sort_by_outcome
...and then in your view you'll use #targets_sorted instead of Target.order(:outcome).

Rails sum of one to many error

I'm has books. Each book has list of chapters. Each chapter has text. I'm need calculate total value of characters. I'm wrote such code:
symbols = 0
b.chapters.all.each do |c|
symbols += c.text.length
end
And that's work fine. But, when i wrought:
symbols = b.chapters.all.sum(:text.length)
It's return invalid count of chars. Did anyone has any suggestion where i'm wrong?
You could write using the block verion of #sum :
b.chapters.sum { |c| c.text.length }
This is wrong : b.chapters.all.sum(:text.length)
Because - :text.length gives you the length of symbol :text as 4. And the 4 is summed up n times, where n is the size of the collection b.chapters.
I tried with the data as I have in my project :
[21] pry(main)> Menu.first.dishes.count # => 5
[22] pry(main)> Menu.first.dishes.map { |d| d.dish_type.size } # => [9, 7, 5, 7, 6]
[23] pry(main)> Menu.first.dishes.sum { |d| d.dish_type.size } # => 34
I have has_many association between Dish and Menu. Now see the below another thing, which made your fool :
Menu.first.dishes.sum(:a.size) # => 5

RoR - print count after group_by

In my Ruby on Rails application, I want to print a count result after I grouped my records in database and I get something like that :
{1=>6}
In my database there is 6 records, all with the same user_id.
Here is my code :
Whatever.group(:user_id).count(:user_id)
I just want to print 1 how to do this. I tried with distinct and uniq without any success...
If you just need to compact that down to a useful result:
Whatever.group(:user_id).count.keys.join(',')
This will handle the case where you have more than one user in the result set.
The count(:user_id) part is redundant unless you're counting based on other conditions. Just use count instead.
Here is an example
('a'..'b').group_by { |i| i * 2 } #=> {"aa"=>["a"], "bb"=>["b"], "cc"=>["c"]}
('a'..'c').group_by { |i| i * 2 }.keys #=> ["aa", "bb", "cc"]
('a'..'c').group_by { |i| i * 2 }.keys[0] #=> "aa"

How to get next values in hash

I have hash for example
{ 1 => 5, 3 => 6, 5 => 5, 8 => 10, 11 => 11}
and I have one key - 5, and I need get hash with next three key-value. in this case result will be:
{ 8 => 10, 11 => 11, 1 => 5}
How i can do this?
That is not a usual use case for a hash table. The whole point is to be able to look up specific keys efficiently, not iterate over keys in sequence.
If you want to do that, you'll need to choose another data structure, either instead of a hash or alongside the hash (if you still wish for efficient lookup).
If you know that they're integer keys, you could test for the existence of subsequent ones until you find three but that's pretty inefficient, especially if the current one is the second highest, for example. You would be better maintaining a different data structure.
as others have said you can't get the 'next' pair of values. If you're looking specifically for numerically ordered pairs where the keys are all numbers, you could do something like this:
h = { 1 => 5, 3 => 6, 5 => 5, 8 => 10, 11 => 11}
sorted_keys = h.keys.sort
sorted_keys.each do |key|
p "#{key} = #{h[key]}"
end
which returns:
"1 = 5"
"3 = 6"
"5 = 5"
"8 = 10"
"11 = 11"
You can't.
Ruby hashes are unordered. There's no reliable "next" key/value.

How do I create spaces between every four integers in Ruby?

I am trying to take the following number:
423523420987
And convert it to this:
4235 2342 0987
It doesn't necessarily have to be an integer either. In fact, I would prefer it to be a string.
You can use String::gsub with a regular expression:
=> 'abcdefghijkl'.gsub(/.{4}(?=.)/, '\0 ')
'abcd efgh ijkl'
class String
def in_groups_of(n, sep=' ')
chars.each_slice(n).map(&:join).join(sep)
end
end
423523420987.to_s.in_groups_of(4) # => '4235 2342 0987'
423523420987.to_s.in_groups_of(5, '-') # => '42352-34209-87'
To expand on #Mark Byer's answer and #glenn mcdonald's comment, what do you want to do if the length of your string/number is not a multiple of 4?
'1234567890'.gsub(/.{4}(?=.)/, '\0 ')
# => "1234 5678 90"
'1234567890'.reverse.gsub(/.{4}(?=.)/, '\0 ').reverse
# => "12 3456 7890"
If you are looking for padded zeros in case you have less than 12 or more than 12 numbers this will help you out:
irb(main):002:0> 423523420987.to_s.scan(/\d{4}/).join(' ')
=> "4235 2342 0987"
irb(main):008:0> ('%d' % 423523420987).scan(/\d{4}/).join(' ')
=> "4235 2342 0987"
Loop through each digit and if the loop index mod 4 = 0 then place a space.
Modulus in Ruby

Resources