Combine first elements of multidimensional array - ruby-on-rails

Let's say I have an array of product IDs and Quantities, like this:
records = [[1, 10], [1, 30], [4, 10], [4, 100], [5, 45]]
What's the easiest/most efficient way in Ruby to achieve a hash of the combined products and quantities, like this?
products_needed = [{id: 1, count:40}, {id: 4, count: 110}, {id:5, count:45}]

Try this:
records.group_by(&:first).map do |id, records_for_id|
{
id: id,
count: records_for_id.sum(&:last)
}
end

If you're in Ruby 2.4+, you can use group_by followed by transform_values:
records.group_by(&:first) # => {1=>[[1, 10], [1, 30]], 4=>[[4, 10], [4, 100]], 5=>[[5, 45]]}
records.group_by(&:first).transform_values do |values|
values.sum(&:last)
end # => {1=>40, 4=>110, 5=>45}

records
.each_with_object(Hash.new(0)){|(k, v), h| h.merge!(k => v){|_, v1, v2| v1 + v2}}
# => {1=>40, 4=>110, 5=>45}
records
.each_with_object(Hash.new(0)){|(k, v), h| h.merge!(k => v){|_, v1, v2| v1 + v2}}
.map{|k, v| {id: k, count: v}}
# => [{:id=>1, :count=>40}, {:id=>4, :count=>110}, {:id=>5, :count=>45}]

You don't have an array of product IDs and Quantities. You have an array of arrays of integers. The easiest way to deal with this array of arrays of integers is to not have an array of arrays of integers but an Order:
class Product
def to_s; 'Some Product' end
alias_method :inspect, :to_s
end
class LineItem
attr_reader :product, :count
def initialize(product, count)
self.product, self.count = product, count
end
def to_s; "#{count} x #{product}" end
alias_method :inspect, :to_s
private
attr_writer :product, :count
end
class Order
include Enumerable
def initialize(*line_items)
self.line_items = line_items
end
def each(&blk) line_items.each(&blk) end
def items
group_by(&:product).map {|product, line_items| LineItem.new(product, line_items.sum(&:count)) }
end
def to_s; line_items.map(&:to_s).join(', ') end
alias_method :inspect, :to_s
private
attr_accessor :line_items
end
Now, assuming that you receive your data in the form of an Order, instead of an array of arrays of integers, like this:
product1 = Product.new
product4 = Product.new
product5 = Product.new
order = Order.new(
LineItem.new(product1, 10),
LineItem.new(product1, 30),
LineItem.new(product4, 10),
LineItem.new(product4, 100),
LineItem.new(product5, 45)
)
All you need to do is:
order.items
#=> [40 x Some Product, 110 x Some Product, 45 x Some Product]
The bottom line is: Ruby is an object-oriented language, not an array-of-arrays-of-integers-oriented language, if you use rich objects instead of arrays of arrays of integers, your problem will become much simpler.
Note: I used order processing as an example. If your problem domain is warehouse management or something else, there will be a similar solution.

Related

printing individual element from nested array in ruby

I have an array like below
attributes_array = {\"rules\":{\"Claim\":[1100,1100],\"Bookmark\":[800,800]}}
I am trying to print Claim & Bookmark and used below but unable to.
first:
attributes_array.each do |var|
puts var.inspect
end
second:
attributes_array.each do |var|
var.each do |val|
puts val
end
end
Any leads would be appreciated.
Refine your question
attributes_array = { rules: { Claim: [1100, 1100], Bookmark: [800,800] } }
If you want to see all values:
attributes_array[:rules].values_at(:Claim, :Bookmark)
#=> [[1100, 1100], [800, 800]]
If you want to see value of :Claim or :Bookmark:
attributes_array[:rules][:Claim]
#=> [1100, 1100]
attributes_array[:rules][:Bookmark]
#=> [800, 800]
If you want to see speciific element of :Claim or :Bookmark:
attributes_array[:rules][:Claim].first
#=> 1100
attributes_array[:rules][:Bookmark].last
#=> 800
If you want hash with only :Claim or :Bookmark:
attributes_array[:rules].slice(:Claim)
#=> {:Claim=>[1100, 1100]}
attributes_array[:rules].slice(:Bookmark)
#=> {:Bookmark=>[800, 800]}

What is the best way to access an element from 2d array saved as a hash value?

I have a hash, its values are 2 dimensional arrays, e.g.
hash = {
"first" => [[1,2,3],[4,5,6]],
"second" => [[7,88,9],[6,2,6]]
}
I want to access the elements to print them in xls file.
I did it in this way:
hash.each do |key, value|
value.each do |arr1|
arr1.each do |arr2|
arr2.each do |arr3|
sheet1.row(row).push arr3
end
end
end
end
Is there a better way to access each single element without using each-statement 4 times?
The desired result is to get each value from key-value pair as an array, e.g.
=> [1,2,3,4,5,6] #first loop
=> [7,88,9,6,2,6] #second loop
#and so on
hash = { "first" =>[[1, 2,3],[4,5,6]],
"second"=>[[7,88,9],[6,2,6]] }
hash.values.map(&:flatten)
#=> [[1, 2, 3, 4, 5, 6], [7, 88, 9, 6, 2, 6]]
Isn't it as simple as something like:
hash.each do |k,v|
sheet1.row(row).concat v.flatten
end

Ruby on Rails group_by is not grouping in the correct order using DateTime

First I get a collection of channels from my scope
Channel.not_archived.map { |c| channels << c }
Then I sort those by the start_time attribute:
channels.sort! { |a, b| a.start_time <=> b.start_time }
Then I want to group them by their start times. So channels that start at 8:00am will be grouped together. So I use the group_by method:
#grouped_channels = #channels.group_by { |c| time_with_offset(c).strftime("%I:%M %P") }
the time_with_offset method:
# Returns time with offset
def time_with_offset(channel)
user = current_user.time_zone.to_i
organization = channel.organization.time_zone.to_i
time_offset = organization -= user
channel.start_time - time_offset.hours
end
And I get back all of my records in the correct group. The issue i'm having is that the groups are not in order. So the group of 8:00am should be before the group of 9:00am. It's just in weird random order now. Can anyone help me get these in correct order?
If you wish to reorder the key-value pairs of any hash h in key order given by an array keys, which contains all the keys in the desired order, write
(keys.zip(h.values_at(*keys))).to_h
For Ruby versions prior to 2.0, write
Hash[keys.zip(h.values_at(*keys))]
For example,
h = { b: 1, d: 2, a: 3, c: 4 }
#=> {:b=>1, :d=>2, :a=>3, :c=>4}
keys = [:a, :b, :c, :d]
(keys.zip(h.values_at(*keys))).to_h
#=> {:a=>3, :b=>1, :c=>4, :d=>2}
The steps are as follows.
a = h.values_at(*keys)
#=> same as h.values_at(:a, :b, :c, :d)
#=> [3, 1, 4, 2]
b = keys.zip(a)
# => [[:a, 3], [:b, 1], [:c, 4], [:d, 2]]
b.to_h
#=> {:a=>3, :b=>1, :c=>4, :d=>2}
First you are sorting by one time, then you are grouping by a different time. I expect this explains your undesired order.
Sort by the offset time.
channels.sort_by { |c| time_with_offset(c) }.group_by { |c| time_with_offset(c).strftime("%I:%M %P") }

Ordering a rails model with string outline notation

I have a rails model that the primary field that the user wants to sort on is a Line Item that is stored in dot-notation format as a string (i.e.: 2.1.4, 2.1.4.1, 2.1.4.5, etc). Ordering alphabetically works great, except that 2.1.4.10 comes before 2.1.4.2 alphabetically. What I want to call 'dot-based numeric order' would put 2.1.4.10 after 2.1.4.9, and 2.4.1.10.1 would precede 2.4.1.11
The question is this: What is The Rails Way™ to set the default order on the model so that the Line Items appear in the correct order, according to 'dot-based numeric order'.
Presume a simple case:
class LineItem < ActiveRecord::Base
validates :line_item, :presence => true, :uniqueness => true
end
and that :line_item is a string.
I assume you are using PostgreSQL and if you really want to set default order for your model, add this default_scope to your LineItem model:
default_scope -> { order("STRING_TO_ARRAY(line_item, '.')::int[] ASC") }
Otherwise I suggest you to use named scope, it can be override and chained:
scope :version_order, -> { order("STRING_TO_ARRAY(line_item, '.')::int[] ASC") }
To do this yourself:
lines = ['3.3.3.3', '3.54.3.3', '3.3.3.20']
sorted = lines.sort do |a, b|
a.split('.').zip(b.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end #=> ["3.3.3.3", "3.3.3.20", "3.54.3.3"]
How it works:
To sort, we pass an array and a block, that blocks gives us 2 arguments that are next to each other in the list, and we can return 0, -1, or 1, which tells Ruby which directions to swap the numbers.
[4,3,-1,2].sort do |x, y|
if x > y
1
elsif x < y
-1
else
0
end
end #=> [-1, 2, 3, 4]
Instead of doing that long logic, Ruby provides a nice operator for us: <=>. Zero means no change, -1 means it's in ascending order, and 1 means the two numbers are in descending order. Ruby repeats that a bunch, and sorts the list.
4 <=> 4 #=> 0
3 <=> 5 #=> -1
5 <=> 3 #=> 1
7 <=> -1 #-> 1
So, we should give higher items (in terms of dots) priority:
#Pseudo Code:
33.44 > 22.55 #=> true
33.44 < 44.33
The easiest way to integrate through all the numbers is an #inject, which gives you a value, and the item you are on. You can do things like this:
[4,4,4].inject(0) {|sum, i| sum + i} #=> 12
[4,4,4].inject(0) {|sum, i| sum - i} #=> -12
['Hello', "I'm penne12"] {|new_word, word| new_word + "-" + word} #=> "Hello-I'm penne12"
So, we'll use an inline if:
(true)? "it's true" : "true is now false. Yay!" #=> "it's true"
(4 > 5)? "logic is weird" : "4 > 5" #=> "4 > 5"
Like this:
.inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
We'll split both strings by the ., to get a list:
"Hello. This. Is. A. Test.".split('.') #=> ["Hello", " This", " Is", " A", "Test"]
"4.4.4.4" #=> [4,4,4,4]
And join the two lists together by element using ruby's #Zip (it's really weird.)
[4,4,4,4].zip([5,5,5,5]) #=> [[4,5], [4,5], [4,5], [4,5]]
You can change what item a and b are, if you want to sort by a different property. Ruby doesn't care what you do to either variable, it only cares about the return value.
a, b = a.line_item, b.line_item
On a model:
class LineItem < ActiveRecord::Base
validates :line_item, :presence => true, :uniqueness => true
def self.sort_by_dbno
self.all.sort do |a, b|
a, b = a.line_item, b.line_item
a.split('.').zip(b.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end
end
end
I overrode the <=> operator with #Penne12's code:
def <=>(y)
self.line_item.split('.').zip(y.line_item.split('.')).inject(0) do |res, val|
(res == 0)? val[0].to_i <=> val[1].to_i : res
end
end
Sorting works on any enumerable collection, with no sort block:
bobs_items = LineItem.where(:owner => bob, :complete => false)
"Bob's workload: #{bobs_items.sort.map { |li| li.line_item }.join(', ')}"

How do I add values in an array when there is a null entry?

I want to create a real time-series array. Currently, I am using the statistics gem to pull out values for each 'day':
define_statistic :sent_count, :count
=> :all, :group => 'DATE(date_sent)',
:filter_on => {:email_id => 'email_id
> = ?'}, :order => 'DATE(date_sent) ASC'
What this does is create an array where there are values for a date, for example
[["12-20-2010",1], ["12-24-2010",3]]
But I need it to fill in the null values, so it looks more like:
[["12-20-2010",1], ["12-21-2010",0], ["12-22-2010",0], ["12-23-2010",0], ["12-24-2010",3]]
Notice how the second example has "0" values for the days that were missing from the first array.
#!/usr/bin/ruby1.8
require 'date'
require 'pp'
def add_missing_dates(series)
series.map do |date, value|
[Date.strptime(date, '%m-%d-%Y'), value]
end.inject([]) do |series, date_and_value|
filler = if series.empty?
[]
else
((series.last[0]+ 1)..(date_and_value[0] - 1)).map do |date|
[date, 0]
end
end
series + filler + [date_and_value]
end.map do |date, value|
[date.to_s, value]
end
end
a = [["12-20-2010",1], ["12-24-2010",3]]
pp add_missing_dates(a)
# => [["2010-12-20", 1],
# => ["2010-12-21", 0],
# => ["2010-12-22", 0],
# => ["2010-12-23", 0],
# => ["2010-12-24", 3]]
I would recommend against monkey-patching the base classes to include this method: It's not all that general purpose; even if it were, it just doesn't need to be there. I'd stick it in a module that you can mix in to whatever code needs it:
module AddMissingDates
def add_missing_dates(series)
...
end
end
class MyClass
include AddMissingDates
...
end
However, if you really want to:
def Array.add_missing_dates(series)
...
end
This works:
#!/usr/bin/env ruby
require 'pp'
require 'date'
# convert the MM-DD-YYYY format date string to a Date
DATE_FORMAT = '%m-%d-%Y'
def parse_date(s)
Date.strptime(s, DATE_FORMAT)
end
dates = [["12-20-2010",1], ["12-24-2010",3]]
# build a hash of the known dates so we can skip the ones that already exist.
date_hash = Hash[*dates.map{ |i| [parse_date(i[0]), i[-1]] }.flatten]
start_date_range = parse_date(dates[0].first)
end_date_range = parse_date(dates[-1].first)
# loop over the date range...
start_date_range.upto(end_date_range) do |d|
# ...and adding entries for the missing ones.
date_hash[d] = 0 if (!date_hash.has_key?(d))
end
# convert the hash back into an array with all dates
all_dates = date_hash.keys.sort.map{ |d| [d.strftime(DATE_FORMAT), date_hash[d] ] }
pp all_dates
# >> [["12-20-2010", 1],
# >> ["12-21-2010", 0],
# >> ["12-22-2010", 0],
# >> ["12-23-2010", 0],
# >> ["12-24-2010", 3]]
Most of the code is preparing things, either to build a new array, or return the date objects back to strings.

Resources