OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
openproject/lib/open_project/xls_export/spreadsheet_builder.rb

179 lines
5.4 KiB

require 'spreadsheet'
# A simple convenience class that wraps some of the spreadsheet
# gem's functionality. It's designed to build spreadsheets incrementally
# by adding row after row, but can be used for random access to the
# rows as well
#
# Multiple Worksheets are possible, the currently active worksheet and it's
# associated column widths are always accessible through the @sheet and @column_widths
# instance variables, the other worksheets are accessible through the #worksheet method.
# If a worksheet with an index larger than the number of worksheets is requested,
# a new worksheet is created.
#
module OpenProject::XlsExport
class SpreadsheetBuilder
Worksheet = Struct.new(:sheet, :column_widths) unless defined? Worksheet
def initialize(name = nil)
Spreadsheet.client_encoding = 'UTF-8'
@xls = Spreadsheet::Workbook.new
@worksheets = []
worksheet(0, name)
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.sheet.default_format.vertical_align = :top
wb.column_widths = []
end
end
@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
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
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
# 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
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")
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
@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.clone
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
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
end
io = StringIO.new
@xls.write(io)
io.rewind
io.read
end
private
def raw_xls
@xls
end
def raw_sheet
@sheet
end
end
end