Skip to content

Commit

Permalink
Merge pull request #368 from roo-rb/refactoring_simplifying_classes
Browse files Browse the repository at this point in the history
Refactoring simplifying classes
  • Loading branch information
stevendaniels authored Dec 31, 2016
2 parents 987c60a + c107923 commit 9a10b05
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 163 deletions.
191 changes: 93 additions & 98 deletions lib/roo/csv.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require 'csv'
require 'time'
require "csv"
require "time"

# The CSV class can read csv files (must be separated with commas) which then
# can be handled like spreadsheets. This means you can access cells like A5
Expand All @@ -9,124 +9,119 @@
#
# You can pass options to the underlying CSV parse operation, via the
# :csv_options option.
#

class Roo::CSV < Roo::Base
module Roo
class CSV < Roo::Base
attr_reader :filename

# Returns an array with the names of the sheets. In CSV class there is only
# one dummy sheet, because a csv file cannot have more than one sheet.
def sheets
["default"]
end

attr_reader :filename
def cell(row, col, sheet = nil)
sheet ||= default_sheet
read_cells(sheet)
@cell[normalize(row, col)]
end

# Returns an array with the names of the sheets. In CSV class there is only
# one dummy sheet, because a csv file cannot have more than one sheet.
def sheets
['default']
end
def celltype(row, col, sheet = nil)
sheet ||= default_sheet
read_cells(sheet)
@cell_type[normalize(row, col)]
end

def cell(row, col, sheet=nil)
sheet ||= default_sheet
read_cells(sheet)
@cell[normalize(row,col)]
end
def cell_postprocessing(_row, _col, value)
value
end

def celltype(row, col, sheet=nil)
sheet ||= default_sheet
read_cells(sheet)
@cell_type[normalize(row,col)]
end
def csv_options
@options[:csv_options] || {}
end

def cell_postprocessing(row,col,value)
value
end
def set_value(row, col, value, _sheet)
@cell[[row, col]] = value
end

def csv_options
@options[:csv_options] || {}
end
def set_type(row, col, type, _sheet)
@cell_type[[row, col]] = type
end

def set_value(row, col, value, _sheet)
@cell[[row, col]] = value
end
private

def set_type(row, col, type, _sheet)
@cell_type[[row, col]] = type
end
TYPE_MAP = {
String => :string,
Float => :float,
Date => :date,
DateTime => :datetime,
}

private
def celltype_class(value)
TYPE_MAP[value.class]
end

TYPE_MAP = {
String => :string,
Float => :float,
Date => :date,
DateTime => :datetime,
}
def read_cells(sheet = default_sheet)
sheet ||= default_sheet
return if @cells_read[sheet]
set_row_count(sheet)
set_column_count(sheet)
row_num = 1

each_row csv_options do |row|
row.each_with_index do |elem, col_num|
coordinate = [row_num, col_num + 1]
@cell[coordinate] = elem
@cell_type[coordinate] = celltype_class(elem)
end
row_num += 1
end

def celltype_class(value)
TYPE_MAP[value.class]
end
@cells_read[sheet] = true
end

def each_row(options, &block)
if uri?(filename)
::Dir.mktmpdir(Roo::TEMP_PREFIX, ENV['ROO_TMP']) do |tmpdir|
tmp_filename = download_uri(filename, tmpdir)
CSV.foreach(tmp_filename, options, &block)
def each_row(options, &block)
if uri?(filename)
each_row_using_temp_dir(filename)
elsif is_stream?(filename_or_stream)
::CSV.new(filename_or_stream, options).each(&block)
else
::CSV.foreach(filename, options, &block)
end
elsif is_stream?(filename_or_stream)
CSV.new(filename_or_stream, options).each(&block)
else
CSV.foreach(filename, options, &block)
end
end

def read_cells(sheet = default_sheet)
sheet ||= default_sheet
return if @cells_read[sheet]
@first_row[sheet] = 1
@last_row[sheet] = 0
@first_column[sheet] = 1
@last_column[sheet] = 1
rownum = 1
each_row csv_options do |row|
row.each_with_index do |elem,i|
@cell[[rownum,i+1]] = cell_postprocessing rownum,i+1, elem
@cell_type[[rownum,i+1]] = celltype_class @cell[[rownum,i+1]]
if i+1 > @last_column[sheet]
@last_column[sheet] += 1
end
def each_row_using_tempdir
::Dir.mktmpdir(Roo::TEMP_PREFIX, ENV["ROO_TMP"]) do |tmpdir|
tmp_filename = download_uri(filename, tmpdir)
::CSV.foreach(tmp_filename, options, &block)
end
rownum += 1
@last_row[sheet] += 1
end
@cells_read[sheet] = true
#-- adjust @first_row if neccessary
while !row(@first_row[sheet]).any? and @first_row[sheet] < @last_row[sheet]
@first_row[sheet] += 1
end
#-- adjust @last_row if neccessary
while !row(@last_row[sheet]).any? and @last_row[sheet] and
@last_row[sheet] > @first_row[sheet]
@last_row[sheet] -= 1
end
#-- adjust @first_column if neccessary
while !column(@first_column[sheet]).any? and
@first_column[sheet] and
@first_column[sheet] < @last_column[sheet]
@first_column[sheet] += 1

def set_row_count(sheet)
@first_row[sheet] = 1
@last_row[sheet] = ::CSV.readlines(@filename).size
@last_row[sheet] = @first_row[sheet] if @last_row[sheet].zero?

nil
end
#-- adjust @last_column if neccessary
while !column(@last_column[sheet]).any? and
@last_column[sheet] and
@last_column[sheet] > @first_column[sheet]
@last_column[sheet] -= 1

def set_column_count(sheet)
@first_column[sheet] = 1
@last_column[sheet] = (::CSV.readlines(@filename).first || []).size
@last_column[sheet] = @first_column[sheet] if @last_column[sheet].zero?

nil
end
end

def clean_sheet(sheet)
read_cells(sheet)
def clean_sheet(sheet)
read_cells(sheet)

@cell.each_pair do |coord, value|
@cell[coord] = sanitize_value(value) if value.is_a?(::String)
end

@cell.each_pair do |coord, value|
@cell[coord] = sanitize_value(value) if value.is_a?(::String)
@cleaned[sheet] = true
end

@cleaned[sheet] = true
alias_method :filename_or_stream, :filename
end

alias_method :filename_or_stream, :filename
end
2 changes: 1 addition & 1 deletion lib/roo/excelx.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def empty?(row, col, sheet = nil)
sheet = sheet_for(sheet)
key = normalize(row, col)
cell = sheet.cells[key]
!cell || cell.empty? || (cell.type == :string && cell.value.empty?) ||
!cell || cell.empty? ||
(row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
end

Expand Down
66 changes: 38 additions & 28 deletions lib/roo/excelx/cell/datetime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,9 @@ def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
#
# Returns a String representation of a cell's value.
def formatted_value
date_regex = /(?<date>[dmy]+[\-\/][dmy]+([\-\/][dmy]+)?)/
time_regex = /(?<time>(\[?[h]\]?+:)?[m]+(:?ss|:?s)?)/

formatter = @format.downcase.split(' ').map do |part|
if part[date_regex] == part
part.gsub(/#{DATE_FORMATS.keys.join('|')}/, DATE_FORMATS)
elsif part[time_regex]
part.gsub(/#{TIME_FORMATS.keys.join('|')}/, TIME_FORMATS)
if (parsed_format = parse_date_or_time_format(part))
parsed_format
else
warn 'Unable to parse custom format. Using "YYYY-mm-dd HH:MM:SS" format.'
return @value.strftime('%F %T')
Expand All @@ -51,35 +46,50 @@ def formatted_value

private

def parse_date_or_time_format(part)
date_regex = /(?<date>[dmy]+[\-\/][dmy]+([\-\/][dmy]+)?)/
time_regex = /(?<time>(\[?[h]\]?+:)?[m]+(:?ss|:?s)?)/

if part[date_regex] == part
formats = DATE_FORMATS
elsif part[time_regex]
formats = TIME_FORMATS
else
return false
end

part.gsub(/#{formats.keys.join('|')}/, formats)
end

DATE_FORMATS = {
'yyyy'.freeze => '%Y'.freeze, # Year: 2000
'yy'.freeze => '%y'.freeze, # Year: 00
'yyyy' => '%Y', # Year: 2000
'yy' => '%y', # Year: 00
# mmmmm => J-D
'mmmm'.freeze => '%B'.freeze, # Month: January
'mmm'.freeze => '%^b'.freeze, # Month: JAN
'mm'.freeze => '%m'.freeze, # Month: 01
'm'.freeze => '%-m'.freeze, # Month: 1
'dddd'.freeze => '%A'.freeze, # Day of the Week: Sunday
'ddd'.freeze => '%^a'.freeze, # Day of the Week: SUN
'dd'.freeze => '%d'.freeze, # Day of the Month: 01
'd'.freeze => '%-d'.freeze, # Day of the Month: 1
'mmmm' => '%B', # Month: January
'mmm' => '%^b', # Month: JAN
'mm' => '%m', # Month: 01
'm' => '%-m', # Month: 1
'dddd' => '%A', # Day of the Week: Sunday
'ddd' => '%^a', # Day of the Week: SUN
'dd' => '%d', # Day of the Month: 01
'd' => '%-d' # Day of the Month: 1
# '\\\\'.freeze => ''.freeze, # NOTE: Fixes a custom format's output.
}

TIME_FORMATS = {
'hh'.freeze => '%H'.freeze, # Hour (24): 01
'h'.freeze => '%-k'.freeze, # Hour (24): 1
'hh' => '%H', # Hour (24): 01
'h' => '%-k'.freeze, # Hour (24): 1
# 'hh'.freeze => '%I'.freeze, # Hour (12): 08
# 'h'.freeze => '%-l'.freeze, # Hour (12): 8
'mm'.freeze => '%M'.freeze, # Minute: 01
'mm' => '%M', # Minute: 01
# FIXME: is this used? Seems like 'm' is used for month, not minute.
'm'.freeze => '%-M'.freeze, # Minute: 1
'ss'.freeze => '%S'.freeze, # Seconds: 01
's'.freeze => '%-S'.freeze, # Seconds: 1
'am/pm'.freeze => '%p'.freeze, # Meridian: AM
'000'.freeze => '%3N'.freeze, # Fractional Seconds: thousandth.
'00'.freeze => '%2N'.freeze, # Fractional Seconds: hundredth.
'0'.freeze => '%1N'.freeze, # Fractional Seconds: tenths.
'm' => '%-M', # Minute: 1
'ss' => '%S', # Seconds: 01
's' => '%-S', # Seconds: 1
'am/pm' => '%p', # Meridian: AM
'000' => '%3N', # Fractional Seconds: thousandth.
'00' => '%2N', # Fractional Seconds: hundredth.
'0' => '%1N' # Fractional Seconds: tenths.
}

def create_datetime(base_date, value)
Expand All @@ -93,7 +103,7 @@ def create_datetime(base_date, value)
def round_datetime(datetime_string)
/(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string

::Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0)
::Time.new(yyyy, mm, dd, hh, mi, ss.to_r).round(0)
end
end
end
Expand Down
41 changes: 18 additions & 23 deletions lib/roo/excelx/cell/number.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def formatted_value
if formatter.is_a? Proc
formatter.call(@cell_value)
elsif zero_padded_number?
"%0#{@format.size}d"% @cell_value
"%0#{@format.size}d" % @cell_value
else
Kernel.format(formatter, @cell_value)
end
Expand All @@ -46,35 +46,19 @@ def formats
'0' => '%.0f',
'0.00' => '%.2f',
'0.000000' => '%.6f',
'#,##0' => proc do |number|
Kernel.format('%.0f', number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end,
'#,##0.00' => proc do |number|
Kernel.format('%.2f', number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end,
'#,##0' => number_format('%.0f'),
'#,##0.00' => number_format('%.2f'),
'0%' => proc do |number|
Kernel.format('%d%', number.to_f * 100)
end,
'0.00%' => proc do |number|
Kernel.format('%.2f%', number.to_f * 100)
end,
'0.00E+00' => '%.2E',
'#,##0 ;(#,##0)' => proc do |number|
formatter = number.to_i > 0 ? '%.0f' : '(%.0f)'
Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end,
'#,##0 ;[Red](#,##0)' => proc do |number|
formatter = number.to_i > 0 ? '%.0f' : '[Red](%.0f)'
Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end,
'#,##0.00;(#,##0.00)' => proc do |number|
formatter = number.to_i > 0 ? '%.2f' : '(%.2f)'
Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end,
'#,##0.00;[Red](#,##0.00)' => proc do |number|
formatter = number.to_i > 0 ? '%.2f' : '[Red](%.2f)'
Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end,
'#,##0 ;(#,##0)' => number_format('%.0f', '(%.0f)'),
'#,##0 ;[Red](#,##0)' => number_format('%.0f', '[Red](%.0f)'),
'#,##0.00;(#,##0.00)' => number_format('%.2f', '(%.2f)'),
'#,##0.00;[Red](#,##0.00)' => number_format('%.2f', '[Red](%.2f)'),
# FIXME: not quite sure what the format should look like in this case.
'##0.0E+0' => '%.1E',
'@' => proc { |number| number }
Expand All @@ -83,6 +67,17 @@ def formats

private

def number_format(formatter, negative_formatter = nil)
proc do |number|
if negative_formatter
formatter = number.to_i > 0 ? formatter : negative_formatter
number = number.to_f.abs
end

Kernel.format(formatter, number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end
end

def zero_padded_number?
@format[/0+/] == @format
end
Expand Down
Loading

0 comments on commit 9a10b05

Please sign in to comment.