|
|
|
@ -11,165 +11,168 @@ require 'spreadsheet' |
|
|
|
|
# If a worksheet with an index larger than the number of worksheets is requested, |
|
|
|
|
# a new worksheet is created. |
|
|
|
|
# |
|
|
|
|
class SpreadsheetBuilder |
|
|
|
|
|
|
|
|
|
Worksheet = Struct.new(:sheet, :column_widths) unless defined? Worksheet |
|
|
|
|
module OpenProject::XlsExport |
|
|
|
|
class SpreadsheetBuilder |
|
|
|
|
|
|
|
|
|
def initialize(name = nil) |
|
|
|
|
Spreadsheet.client_encoding = 'UTF-8' |
|
|
|
|
@xls = Spreadsheet::Workbook.new |
|
|
|
|
@worksheets = [] |
|
|
|
|
worksheet(0, name) |
|
|
|
|
end |
|
|
|
|
Worksheet = Struct.new(:sheet, :column_widths) unless defined? Worksheet |
|
|
|
|
|
|
|
|
|
# Retrieve or create the worksheet at index x |
|
|
|
|
def worksheet(idx, name = nil) |
|
|
|
|
name ||= "Worksheet #{@worksheets.length + 1}" |
|
|
|
|
if @worksheets[idx].nil? |
|
|
|
|
@worksheets[idx] = Worksheet.new.tap do |wb| |
|
|
|
|
wb.sheet = @xls.create_worksheet(:name => name) |
|
|
|
|
wb.column_widths = [] |
|
|
|
|
end |
|
|
|
|
def initialize(name = nil) |
|
|
|
|
Spreadsheet.client_encoding = 'UTF-8' |
|
|
|
|
@xls = Spreadsheet::Workbook.new |
|
|
|
|
@worksheets = [] |
|
|
|
|
worksheet(0, name) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@sheet = @worksheets[idx].sheet |
|
|
|
|
@column_widths = @worksheets[idx].column_widths |
|
|
|
|
end |
|
|
|
|
# Retrieve or create the worksheet at index x |
|
|
|
|
def worksheet(idx, name = nil) |
|
|
|
|
name ||= "Worksheet #{@worksheets.length + 1}" |
|
|
|
|
if @worksheets[idx].nil? |
|
|
|
|
@worksheets[idx] = Worksheet.new.tap do |wb| |
|
|
|
|
wb.sheet = @xls.create_worksheet(:name => name) |
|
|
|
|
wb.column_widths = [] |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Update column widths and wrap text if neccessary |
|
|
|
|
def update_sheet_widths |
|
|
|
|
@column_widths.count.times do |idx| |
|
|
|
|
if @column_widths[idx] > 60 |
|
|
|
|
@sheet.column(idx).width = 60 |
|
|
|
|
@sheet.rows.each do |row| |
|
|
|
|
fmt = row.formats[idx] || @sheet.column(idx).default_format |
|
|
|
|
fmt.text_wrap = true |
|
|
|
|
row.set_format(idx, fmt) |
|
|
|
|
@sheet = @worksheets[idx].sheet |
|
|
|
|
@column_widths = @worksheets[idx].column_widths |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Update column widths and wrap text if neccessary |
|
|
|
|
def update_sheet_widths |
|
|
|
|
@column_widths.count.times do |idx| |
|
|
|
|
if @column_widths[idx] > 60 |
|
|
|
|
@sheet.column(idx).width = 60 |
|
|
|
|
@sheet.rows.each do |row| |
|
|
|
|
fmt = row.formats[idx] || @sheet.column(idx).default_format |
|
|
|
|
fmt.text_wrap = true |
|
|
|
|
row.set_format(idx, fmt) |
|
|
|
|
end |
|
|
|
|
else |
|
|
|
|
@sheet.column(idx).width = @column_widths[idx] |
|
|
|
|
end |
|
|
|
|
else |
|
|
|
|
@sheet.column(idx).width = @column_widths[idx] |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Get the approximate width of a value as seen in the excel sheet |
|
|
|
|
def get_value_width(value) |
|
|
|
|
if ['Time', 'Date'].include?(value.class.name) |
|
|
|
|
return 18 unless value.to_s.length < 18 |
|
|
|
|
# Get the approximate width of a value as seen in the excel sheet |
|
|
|
|
def get_value_width(value) |
|
|
|
|
if ['Time', 'Date'].include?(value.class.name) |
|
|
|
|
return 18 unless value.to_s.length < 18 |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
tot_w = [Float(0)] |
|
|
|
|
idx=0 |
|
|
|
|
value.to_s.each_char do |c| |
|
|
|
|
case c |
|
|
|
|
when '0'..'9' |
|
|
|
|
tot_w[idx] += 1.2 |
|
|
|
|
when '.', ';', ':', ',', ' ', 'i', 'I', 'j', 'J', '(', ')', '[', ']', '!', '-', 't', 'l' |
|
|
|
|
tot_w[idx] += 0.7 |
|
|
|
|
when 'W', 'M', 'D' |
|
|
|
|
tot_w[idx] += 1.2 |
|
|
|
|
when "\n" |
|
|
|
|
idx = idx + 1 |
|
|
|
|
tot_w << Float(0) |
|
|
|
|
else |
|
|
|
|
tot_w[idx] += 1.05 |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
wdth=0 |
|
|
|
|
tot_w.each do |w| |
|
|
|
|
wdth = w unless w<wdth |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
return wdth+1.5 |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
tot_w = [Float(0)] |
|
|
|
|
idx=0 |
|
|
|
|
value.to_s.each_char do |c| |
|
|
|
|
case c |
|
|
|
|
when '0'..'9' |
|
|
|
|
tot_w[idx] += 1.2 |
|
|
|
|
when '.', ';', ':', ',', ' ', 'i', 'I', 'j', 'J', '(', ')', '[', ']', '!', '-', 't', 'l' |
|
|
|
|
tot_w[idx] += 0.7 |
|
|
|
|
when 'W', 'M', 'D' |
|
|
|
|
tot_w[idx] += 1.2 |
|
|
|
|
when "\n" |
|
|
|
|
idx = idx + 1 |
|
|
|
|
tot_w << Float(0) |
|
|
|
|
# Add a "Title". This basically just set the first column to |
|
|
|
|
# the passed text and makes it bold and larger (font-size 18) |
|
|
|
|
def add_title(arr_or_str) |
|
|
|
|
if arr_or_str.respond_to? :to_str |
|
|
|
|
@sheet[0, 0] = arr_or_str |
|
|
|
|
else |
|
|
|
|
tot_w[idx] += 1.05 |
|
|
|
|
@sheet.row(0).concat arr_or_str |
|
|
|
|
value_width = get_value_width(arr_or_str[0] * 2) |
|
|
|
|
@column_widths[0] = value_width if (@column_widths[0] || 0) < value_width |
|
|
|
|
end |
|
|
|
|
title_format = Spreadsheet::Format.new(:weight => :bold, :size => 18) |
|
|
|
|
@sheet.row(0).set_format(0, title_format) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
wdth=0 |
|
|
|
|
tot_w.each do |w| |
|
|
|
|
wdth = w unless w<wdth |
|
|
|
|
# Add an empty row in the next sequential position. Convenience method |
|
|
|
|
# for calling add_row([""]) |
|
|
|
|
def add_empty_row |
|
|
|
|
add_row([""]) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
return wdth+1.5 |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Add a "Title". This basically just set the first column to |
|
|
|
|
# the passed text and makes it bold and larger (font-size 18) |
|
|
|
|
def add_title(arr_or_str) |
|
|
|
|
if arr_or_str.respond_to? :to_str |
|
|
|
|
@sheet[0, 0] = arr_or_str |
|
|
|
|
else |
|
|
|
|
@sheet.row(0).concat arr_or_str |
|
|
|
|
value_width = get_value_width(arr_or_str[0] * 2) |
|
|
|
|
@column_widths[0] = value_width if (@column_widths[0] || 0) < value_width |
|
|
|
|
# Add headers. This is usually used for adding a table header to the |
|
|
|
|
# second row in the document, but the row can be set using the second |
|
|
|
|
# optional parameter. The format is automatically set to bold font |
|
|
|
|
def add_headers(arr, idx = nil) |
|
|
|
|
header_format = Spreadsheet::Format.new(:weight => :bold) |
|
|
|
|
add_row(arr, idx) |
|
|
|
|
idx ||= @sheet.last_row_index |
|
|
|
|
(arr.size + 1).times { |i| @sheet.row(idx).set_format(i, header_format) } |
|
|
|
|
end |
|
|
|
|
title_format = Spreadsheet::Format.new(:weight => :bold, :size => 18) |
|
|
|
|
@sheet.row(0).set_format(0, title_format) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Add an empty row in the next sequential position. Convenience method |
|
|
|
|
# for calling add_row([""]) |
|
|
|
|
def add_empty_row |
|
|
|
|
add_row([""]) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Add headers. This is usually used for adding a table header to the |
|
|
|
|
# second row in the document, but the row can be set using the second |
|
|
|
|
# optional parameter. The format is automatically set to bold font |
|
|
|
|
def add_headers(arr, idx = nil) |
|
|
|
|
header_format = Spreadsheet::Format.new(:weight => :bold) |
|
|
|
|
add_row(arr, idx) |
|
|
|
|
idx ||= @sheet.last_row_index |
|
|
|
|
(arr.size + 1).times { |i| @sheet.row(idx).set_format(i, header_format) } |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Add a simple row. This will default to the next row in the sequence. |
|
|
|
|
# Fixnums, Dates and Times are preserved, all other types are converted |
|
|
|
|
# to String as the spreadsheet gem cannot do more formats |
|
|
|
|
def add_row(arr, idx = nil) |
|
|
|
|
idx ||= [@sheet.last_row_index + 1, 1].max |
|
|
|
|
column_array = [] |
|
|
|
|
arr.each_with_index do |c,i| |
|
|
|
|
value = if ['Time', 'Date', 'Fixnum', 'Float', 'Integer'].include?(c.class.name) |
|
|
|
|
c |
|
|
|
|
elsif c.class == BigDecimal |
|
|
|
|
c.to_f |
|
|
|
|
else |
|
|
|
|
c.to_s.gsub('_', ' ').gsub("\r\n", "\n").gsub("\r", "\n") |
|
|
|
|
# Add a simple row. This will default to the next row in the sequence. |
|
|
|
|
# Fixnums, Dates and Times are preserved, all other types are converted |
|
|
|
|
# to String as the spreadsheet gem cannot do more formats |
|
|
|
|
def add_row(arr, idx = nil) |
|
|
|
|
idx ||= [@sheet.last_row_index + 1, 1].max |
|
|
|
|
column_array = [] |
|
|
|
|
arr.each_with_index do |c,i| |
|
|
|
|
value = if ['Time', 'Date', 'Fixnum', 'Float', 'Integer'].include?(c.class.name) |
|
|
|
|
c |
|
|
|
|
elsif c.class == BigDecimal |
|
|
|
|
c.to_f |
|
|
|
|
else |
|
|
|
|
c.to_s.gsub('_', ' ').gsub("\r\n", "\n").gsub("\r", "\n") |
|
|
|
|
end |
|
|
|
|
column_array << value |
|
|
|
|
@column_widths[i] = 0 if @column_widths[i].nil? |
|
|
|
|
value_width = get_value_width(value) |
|
|
|
|
@column_widths[i] = value_width if @column_widths[i] < value_width |
|
|
|
|
end |
|
|
|
|
column_array << value |
|
|
|
|
@column_widths[i] = 0 if @column_widths[i].nil? |
|
|
|
|
value_width = get_value_width(value) |
|
|
|
|
@column_widths[i] = value_width if @column_widths[i] < value_width |
|
|
|
|
@sheet.row(idx).concat column_array |
|
|
|
|
end |
|
|
|
|
@sheet.row(idx).concat column_array |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Add a default format to the column at index |
|
|
|
|
def add_format_option_to_column(index, opt) |
|
|
|
|
unless opt.empty? |
|
|
|
|
fmt = @sheet.column(index).default_format |
|
|
|
|
opt.each do |k,v| |
|
|
|
|
fmt.send(:"#{k.to_sym}=", v) if fmt.respond_to? :"#{k.to_sym}=" |
|
|
|
|
# Add a default format to the column at index |
|
|
|
|
def add_format_option_to_column(index, opt) |
|
|
|
|
unless opt.empty? |
|
|
|
|
fmt = @sheet.column(index).default_format |
|
|
|
|
opt.each do |k,v| |
|
|
|
|
fmt.send(:"#{k.to_sym}=", v) if fmt.respond_to? :"#{k.to_sym}=" |
|
|
|
|
end |
|
|
|
|
@sheet.column(index).default_format = fmt |
|
|
|
|
end |
|
|
|
|
@sheet.column(index).default_format = fmt |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Return the next free row we would write to in natural indexing (Starting at 1) |
|
|
|
|
def current_row |
|
|
|
|
@sheet.row_count |
|
|
|
|
end |
|
|
|
|
# Return the next free row we would write to in natural indexing (Starting at 1) |
|
|
|
|
def current_row |
|
|
|
|
@sheet.row_count |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Return the xls file as a string |
|
|
|
|
def xls |
|
|
|
|
@worksheets.length.times do |i| |
|
|
|
|
worksheet(i) |
|
|
|
|
update_sheet_widths |
|
|
|
|
# Return the xls file as a string |
|
|
|
|
def xls |
|
|
|
|
@worksheets.length.times do |i| |
|
|
|
|
worksheet(i) |
|
|
|
|
update_sheet_widths |
|
|
|
|
end |
|
|
|
|
io = StringIO.new |
|
|
|
|
@xls.write(io) |
|
|
|
|
io.rewind |
|
|
|
|
io.read |
|
|
|
|
end |
|
|
|
|
io = StringIO.new |
|
|
|
|
@xls.write(io) |
|
|
|
|
io.rewind |
|
|
|
|
io.read |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
private |
|
|
|
|
def raw_xls |
|
|
|
|
@xls |
|
|
|
|
end |
|
|
|
|
private |
|
|
|
|
def raw_xls |
|
|
|
|
@xls |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def raw_sheet |
|
|
|
|
@sheet |
|
|
|
|
def raw_sheet |
|
|
|
|
@sheet |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|