Exporting to CSV and Summing hours in Ruby on Rails - ruby

I have a Rails 3.2.21 app where I'm building time clock functionality. I'm currently writing a to_csv method that should do the following:
Create a header row with column names
Iterate through a block of input (records) and display the employee username, clock_in, clock_out, station, and comment objects, then finally on the last line of the block display the total hours.
In between each user I want to display a sum of their total hours. As you can see in the to_csv method I'm able to get this to work "hackish" by shoveling an array of csv << [TimeFormatter.format_time(ce.user.clock_events.sum(&:total_hours))] into the CSV. The end result is it does give me the proper total hours for each employee's clock_events, but it repeats it after every entry because I'm obviously iterating over a block.
I'd like to figure out a way to abstract this outside of the block and figure out how to shovel in another array that calculates total_hours for all clock events by user without duplicate entries.
Below is my model, so if something is not clear, please let me know. Also if my question is confusing or doesn't make sense let me know and I'll be happy to clarify.
class ClockEvent < ActiveRecord::Base
attr_accessible :clock_in, :clock_out, :user_id, :station_id, :comment
belongs_to :user
belongs_to :station
scope :incomplete, -> { where(clock_out: nil) }
scope :complete, -> { where("clock_out IS NOT NULL") }
scope :current_week, -> {where("clock_in BETWEEN ? AND ?", Time.zone.now.beginning_of_week - 1.day, Time.zone.now.end_of_week - 1.day)}
scope :search_between, lambda { |start_date, end_date| where("clock_in BETWEEN ? AND ?", start_date.beginning_of_day, end_date.end_of_day)}
scope :search_by_start_date, lambda { |start_date| where('clock_in BETWEEN ? AND ?', start_date.beginning_of_day, start_date.end_of_day) }
scope :search_by_end_date, lambda { |end_date| where('clock_in BETWEEN ? AND ?', end_date.beginning_of_day, end_date.end_of_day) }
def punch_in(station_id)
self.clock_in = Time.zone.now
self.station_id = station_id
end
def punch_out
self.clock_out = Time.zone.now
end
def completed?
clock_in.present? && clock_out.present?
end
def total_hours
self.clock_out.to_i - self.clock_in.to_i
end
def formatted_clock_in
clock_in.try(:strftime, "%m/%d/%y-%H:%M")
end
def formatted_clock_out
clock_out.try(:strftime, "%m/%d/%y-%H:%M")
end
def self.search(search)
search ||= { type: "all" }
results = scoped
# If searching with BOTH a start and end date
if search[:start_date].present? && search[:end_date].present?
results = results.search_between(Date.parse(search[:start_date]), Date.parse(search[:end_date]))
# If search with any other date parameters (including none)
else
results = results.search_by_start_date(Date.parse(search[:start_date])) if search[:start_date].present?
results = results.search_by_end_date(Date.parse(search[:end_date])) if search[:end_date].present?
end
results
end
def self.to_csv(records = [], options = {})
CSV.generate(options) do |csv|
csv << ["Employee", "Clock-In", "Clock-Out", "Station", "Comment", "Total Shift Hours"]
records.each do |ce|
csv << [ce.user.try(:username), ce.formatted_clock_in, ce.formatted_clock_out, ce.station.try(:station_name), ce.comment, TimeFormatter.format_time(ce.total_hours)]
csv << [TimeFormatter.format_time(ce.user.clock_events.sum(&:total_hours))]
end
csv << [TimeFormatter.format_time(records.sum(&:total_hours))]
end
end
end

With some help from a friend and researching APIdocs I was able to refactor the method as so:
def self.to_csv(records = [], options = {})
CSV.generate(options) do |csv|
csv << ["Employee", "Clock-In", "Clock-Out", "Station", "Comment", "Total Shift Hours"]
# records.group_by{ |r| r.user }.each do |user, records|
records.each do |ce|
csv << [ce.user.try(:username), ce.formatted_clock_in, ce.formatted_clock_out, ce.station.try(:station_name), ce.comment, TimeFormatter.format_time(ce.total_hours)]
#csv << [TimeFormatter.format_time(records.select{ |r| r.user == ce.user }.sum(&:total_hours))]
end
records.map(&:user).uniq.each do |user|
csv << ["Total Hours for: #{user.username}"]
csv << [TimeFormatter.format_time(records.select{ |r| r.user == user}.sum(&:total_hours))]
end
csv << ["Total Payroll Hours"]
csv << [TimeFormatter.format_time(records.sum(&:total_hours))]
end
end

Related

How to find duplicate properties in object

all objects
[#<User:0x0000000002afbae8
#uid="john">,
#name="john doe">,
#<User:0x0000000002b026e0
#uid="mike">,
#name="mike spencer">,
#<User:0x0000000002b012e0
#uid="mike">,
#name="mike ferrell">,
]
currently i only get the last saved object,
[<User:0x0000000002b012e0
#uid="mike">,
#name="mike ferrell">,
]
What would be an easiest way to check if object has duplicate properties, and if so, return all of them?
Thank you!
class User
attr_accessor :uid,:name
##all = []
def initialize(uid, name)
#uid = uid
#name = name
##all << self
end
def self.select_duplicate
seen = []
duplicates = []
##all.each do |user|
if seen.include?(user.uid)
duplicates << user
else
seen << user.uid
end
end
end
end
Make a Hash whose key is the uid and the value is an Array of users with that uid. Then only the values which are larger than one are duplicates.
users_by_uid = ##all.each_with_object({}) { |user,m|
m[user.uid] << user
}
dup_uids = users_by_uid.filter { |uid,users|
users.size > 1
}
dup_uids.each { |uid,users|
puts "Users #{users.map(&name).join(", ")} have the same uid #{uid}"
}
And, as #AlexGolubenko pointed out, you can replace the each_with_object with group_by. It does the same thing, but more compact, and perhaps a touch faster.
users_by_uid = ##all.group_by(&:uid)

Is there any way to replace attributes with item objects inside this display_table method?

Creating a program to get calculate sales tax as per provided details and some standard rates defined in this program. Now I want to simplify this format display class by replacing attributes inside "#max_length_fields[#data_array[0][i]]}s" and replace it with objects.
I have tried to push all fields directly inside this class but that further complicates the code.
The Data array used within Format display class is pushed from another Invoice class containing method names as create_data_array :
def create_data_array
data_array = []
data_array.push(["name", "imported", "exempted", "price", "price including tax"])
#item_list.each do |item|
data_array.push([item.name, item.imported, item.exempted, item.price, item.price_including_tax])
end
data_array
end
Now I want to simplify the code by using item objects rather using attributes inside array, class below displays final output of this program and need to be rectified -
class FormatDisplay
def initialize(data_array)
#data_array = data_array
#max_length_fields = {}
#column_width = 0
#data_array[0].each { |field| #max_length_fields[field] = field.length}
end
def display_table
#data_array.each do |fieldset|
row_string = ''
fieldset.each_with_index do |field, i|
row_string += '| ' + sprintf("%#{#max_length_fields[#data_array[0][i]]}s", field) + " "
end
puts row_string
end
end
end
Expected Result : Replacing attribute from "#max_length_fields[#data_array[0][i]]}s" with item object something like option.title
If I understood you right this would work
def create_data_array
item_list.map do |item|
{
name: item.name,
imported: item.imported,
exempted: item.exempted,
price: item.price,
price_including_tax: item.price_including_tax
}
end
end
class FormatDisplay
def initialize(data_array)
#data_array = data_array
#column_width = 0
end
def display_table
#data_array.each do |fieldset|
row_string = ''
fieldset.each_pair do |key, value|
row_string += '| ' + sprintf("%#{ key.size }s", value) + ' '
end
puts row_string
end
end
end

How to call hash values outside class from defined hash map inside class methods?

Read a csv format file and construct a new class with the name of the file dynamically. So if the csv is persons.csv, the ruby class should be person, if it's places.csv, the ruby class should be places
Also create methods for reading and displaying each value in "csv" file and values in first row of csv file will act as name of the function.
Construct an array of objects and associate each object with the row of a csv file. For example the content of the csv file could be
name,age,city
abd,45,TUY
kjh,65,HJK
Previous code :
require 'csv'
class Feed
def initialize(source_name, column_names = [])
if column_names.empty?
column_names = CSV.open(source_name, 'r', &:first)
end
columns = column_names.reduce({}) { |columns, col_name| columns[col_name] = []; columns }
define_singleton_method(:columns) { column_names }
column_names.each do |col_name|
define_singleton_method(col_name.to_sym) { columns[col_name] }
end
CSV.foreach(source_name, headers: true) do |row|
column_names.each do |col_name|
columns[col_name] << row[col_name]
end
end
end
end
feed = Feed.new('input.csv')
puts feed.columns #["name", "age", "city"]
puts feed.name # ["abd", "kjh"]
puts feed.age # ["45", "65"]
puts feed.city # ["TUY", "HJK"]
I am trying to refine this solution using class methods and split code into smaller methods. Calling values outside the class using key names but facing errors like "undefined method `age' for Feed:Class". Is that a way I can access values outside the class ?
My solution looks like -
require 'csv'
class Feed
attr_accessor :column_names
def self.col_name(source_name, column_names = [])
if column_names.empty?
#column_names = CSV.open(source_name, :headers => true)
end
columns = #column_names.reduce({}) { |columns, col_name| columns[col_name] = []; columns }
end
def self.get_rows(source_name)
col_name(source_name, column_names = [])
define_singleton_method(:columns) { column_names }
column_names.each do |col_name|
define_singleton_method(col_name.to_sym) { columns[col_name] }
end
CSV.foreach(source_name, headers: true) do |row|
#column_names.each do |col_name|
columns[col_name] << row[col_name]
end
end
end
end
obj = Feed.new
Feed.get_rows('Input.csv')
puts obj.class.columns
puts obj.class.name
puts obj.class.age
puts obj.class.city
Expected Result -
input = Input.new
p input.name # ["abd", "kjh"]
p input.age # ["45", "65"]
input.name ='XYZ' # Value must be appended to array
input.age = 25
p input.name # ["abd", "kjh", "XYZ"]
p input.age # ["45", "65", "25"]
Let's create the CSV file.
str =<<END
name,age,city
abd,45,TUY
kjh,65,HJK
END
FName = 'temp/persons.csv'
File.write(FName, str)
#=> 36
Now let's create a class:
klass = Class.new
#=> #<Class:0x000057d0519de8a0>
and name it:
class_name = File.basename(FName, ".csv").capitalize
#=> "Persons"
Object.const_set(class_name, klass)
#=> Persons
Persons.class
#=> Class
See File::basename, String#capitalize and Module#const_set.
Next read the CSV file with headers into a CSV::Table object:
require 'csv'
csv = CSV.read(FName, headers: true)
#=> #<CSV::Table mode:col_or_row row_count:3>
csv.class
#=> CSV::Table
See CSV#read. We may now create the methods name, age and city.
csv.headers.each { |header| klass.define_method(header) { csv[header] } }
See CSV#headers, Module::define_method and CSV::Row#[].
We can now confirm they work as intended:
k = klass.new
k.name
#=> ["abd", "kjh"]
k.age
#=> ["45", "65"]
k.city
#=> ["TUY", "HJK"]
or
p = Persons.new
#=> #<Persons:0x0000598dc6b01640>
p.name
#=> ["abd", "kjh"]
and so on.

Delete an array from a class in ruby

So I am creating a class in ruby where in I will be able to insert my data on a text file and then read, find from it but I am stuck on delete as well update/edit.
Basically I created a method called "find" and I made it as a reference on my "delete" method.
def find(keyword="")
if keyword
person = People.read_people
found = person.select do |pip|
pip.name.downcase.include?(keyword.downcase) ||
pip.age.downcase.include?(keyword.downcase) ||
pip.country.downcase.include?(keyword.downcase)
end
found.each do |person|
puts person.name + " | " + person.age + " | " + person.country
end
else
puts "find using a key phrase eg. 'find sam' \n\n"
end
end
def list
puts "\nListing People \n\n".upcase
people = People.read_people
people.each do |person|
puts person.name + " | " + person.age + " | " + person.country
end
end
def delete(keyword="")
if keyword
person = People.read_people
found = person.select do |pip|
pip.name.downcase.include?(keyword.downcase) ||
pip.age.downcase.include?(keyword.downcase) ||
pip.country.downcase.include?(keyword.downcase)
end
person.delete(found)
else
puts "find using a key phrase eg. 'find josh' \n\n"
end
end
As you can see I was trying to delete the supplied keyword from the array (w/c was save on a text file) via class method called read_people. Here's how it looks like:
def self.read_people
# read the people file
# return instances of people
people = []
if file_usable?
file = File.new(##filepath, 'r')
file.each_line do |line|
people << People.new.import_line(line.chomp)
end
file.close
end
return people
end
def import_line(line)
line_array = line.split("\t")
#name, #age, #country = line_array
return self
end
How can I fix this and delete the found item via keyword?
See the actual codes here: https://repl.it/repls/VastWildFact
Change
person.delete(found)
to
person -= found # Equivalent to person = person - found
It should work as per https://ruby-doc.org/core-2.2.0/Array.html#method-i-2D
ary - other_ary → new_ary
Returns a new array that is a copy of the original array, removing any items that also appear in other_ary. The order is preserved from the original array.
It compares elements using their hash and eql? methods for efficiency.
Example: [ 1, 1, 2, 2, 3, 3, 4, 5 ] - [ 1, 2, 4 ] #=> [ 3, 3, 5 ]
Another solution is to use reject as follows:
person.reject! do |pip|
pip.name.downcase.include?(keyword.downcase) ||
pip.age.downcase.include?(keyword.downcase) ||
pip.country.downcase.include?(keyword.downcase)
end
Basically you're going to want an export_people and write_people method that'll look something like this:
def self.export_people(people)
people.map do |person|
[person.name, person.age, person.country].join("\t")
end
end
def self.write_people(people)
File.new(##filepath, 'w') do |f|
f.write(export_people(people))
end
end
# Usage:
Person.write_people(array_of_people)
With the above code, you'd call the modified delete method as detailed in Tarek's answer, and then Person.write_people(array_of_people) to write back to the file.

Ruby how to merge two CSV files with slightly different headers

I have two CSV files with some common headers and others that only appear in one or in the other, for example:
# csv_1.csv
H1,H2,H3
V11,V22,V33
V14,V25,V35
# csv_2.csv
H1,H4
V1a,V4b
V1c,V4d
I would like to merge both and obtain a new CSV file that combines all the information for the previous CSV files. Injecting new columns when needed, and feeding the new cells with null values.
Result example:
H1,H2,H3,H4
V11,V22,V33,
V14,V25,V35,
V1a,,,V4b
V1c,,,V4d
Challenge accepted :)
#!/usr/bin/env ruby
require "csv"
module MergeCsv
class << self
def run(csv_paths)
csv_files = csv_paths.map { |p| CSV.read(p, headers: true) }
merge(csv_files)
end
private
def merge(csv_files)
headers = csv_files.flat_map(&:headers).uniq.sort
hash_array = csv_files.flat_map(&method(:csv_to_hash_array))
CSV.generate do |merged_csv|
merged_csv << headers
hash_array.each do |row|
merged_csv << row.values_at(*headers)
end
end
end
# Probably not the most performant way, but easy
def csv_to_hash_array(csv)
csv.to_a[1..-1].map { |row| csv.headers.zip(row).to_h }
end
end
end
if(ARGV.length == 0)
puts "Use: ruby merge_csv.rb <file_path_csv_1> <file_path_csv_2>"
exit 1
end
puts MergeCsv.run(ARGV)
I have the answer, I just wanted to help people that is looking for the same solution
require "csv"
module MergeCsv
def self.run(csv_1_path, csv_2_path)
merge(File.read(csv_1_path), File.read(csv_2_path))
end
def self.merge(csv_1, csv_2)
csv_1_table = CSV.parse(csv_1, :headers => true)
csv_2_table = CSV.parse(csv_2, :headers => true)
return csv_2_table.to_csv if csv_1_table.headers.empty?
return csv_1_table.to_csv if csv_2_table.headers.empty?
headers_in_1_not_in_2 = csv_1_table.headers - csv_2_table.headers
headers_in_1_not_in_2.each do |header_in_1_not_in_2|
csv_2_table[header_in_1_not_in_2] = nil
end
headers_in_2_not_in_1 = csv_2_table.headers - csv_1_table.headers
headers_in_2_not_in_1.each do |header_in_2_not_in_1|
csv_1_table[header_in_2_not_in_1] = nil
end
csv_2_table.each do |csv_2_row|
csv_1_table << csv_1_table.headers.map { |csv_1_header| csv_2_row[csv_1_header] }
end
csv_1_table.to_csv
end
end
if(ARGV.length != 2)
puts "Use: ruby merge_csv.rb <file_path_csv_1> <file_path_csv_2>"
exit 1
end
puts MergeCsv.run(ARGV[0], ARGV[1])
And execute it from the console this way:
$ ruby merge_csv.rb csv_1.csv csv_2.csv
Any other, maybe cleaner, solution is welcome.
Simplied first answer:
How to use it:
listPart_A = CSV.read(csv_path_A, headers:true)
listPart_B = CSV.read(csv_path_B, headers:true)
listPart_C = CSV.read(csv_path_C, headers:true)
list = merge(listPart_A,listPart_B,listPart_C)
Function:
def merge(*csvs)
headers = csvs.map {|csv| csv.headers }.flatten.compact.uniq.sort
csvs.flat_map(&method(:csv_to_hash_array))
end
def csv_to_hash_array(csv)
csv.to_a[1..-1].map do |row|
Hash[csv.headers.zip(row)]
end
end
I had to do something very similar
to merge n CSV files that the might share some of the columns but some may not
if you want to keep a structure and do it easily,
I think the best way is to convert to hash and then re-convert to CSV file
my solution:
#!/usr/bin/env ruby
require "csv"
def join_multiple_csv(csv_path_array)
return nil if csv_path_array.nil? or csv_path_array.empty?
f = CSV.parse(File.read(csv_path_array[0]), :headers => true)
f_h = {}
f.headers.each {|header| f_h[header] = f[header]}
n_rows = f.size
csv_path_array.shift(1)
csv_path_array.each do |csv_file|
curr_csv = CSV.parse(File.read(csv_file), :headers => true)
curr_h = {}
curr_csv.headers.each {|header| curr_h[header] = curr_csv[header]}
new_headers = curr_csv.headers - f_h.keys
exist_headers = curr_csv.headers - new_headers
new_headers.each { |new_header|
f_h[new_header] = Array.new(n_rows) + curr_csv[new_header]
}
exist_headers.each {|exist_header|
f_h[exist_header] = f_h[exist_header] + curr_csv[exist_header]
}
n_rows = n_rows + curr_csv.size
end
csv_string = CSV.generate do |csv|
csv << f_h.keys
(0..n_rows-1).each do |i|
row = []
f_h.each_key do |header|
row << f_h[header][i]
end
csv << row
end
end
return csv_string
end
if(ARGV.length < 2)
puts "Use: ruby merge_csv.rb <file_path_csv_1> <file_path_csv_2> .. <file_path_csv_n>"
exit 1
end
csv_str = join_multiple_csv(ARGV)
f = File.open("results.csv", "w")
f.write(csv_str)
puts "CSV merge is done"

Resources