package Lire::ReportParser::ExcelWriter; use strict; use vars qw/ $DATE_SYSTEM @name_cols_width/; use base qw/ Lire::ReportParser::RowColHandler /; use Lire::ReportParser::AsciiDocBookFormatter qw/dbk2txt/; use POSIX qw/ceil/; use Spreadsheet::WriteExcel; #use Time::Timezone; use constant MAX_EXCEL_STRING => 255; use constant MAX_SHEET_STRING => 31; use constant DEFAULT_ROW_HEIGHT => 12.75; # Those values are in Excel Points #use constant CHAR_WIDTH => 5.3; #use constant DEFAULT_CELL_WIDTH => 48; #use constant MAX_CELL_WIDTH => 4 * 48; # Spreadsheet::WriteExcel seems to work with characters use constant CHAR_WIDTH => 1; use constant DEFAULT_CELL_WIDTH => 9; use constant MAX_CELL_WIDTH => 36; BEGIN { # Use 1900 date system on all platforms other than Apple Mac (for which # use 1904 date system). $DATE_SYSTEM = ($^O eq 'MacOS') ? 1 : 0; } # # This was inspired by the comments and programs datecalc1.pl # included in Spreadsheet::WriteExcel and is # Copyright (c) 2000, Andrew Benham. # This program is free software. It may be used, redistributed and/or # modified under the same terms as Perl itself. # # It was modified to use Time::Timezone which is included in Lire. # sub epoch2excel_date { my ($time) = @_; # Divide timestamp by number of seconds in a day. # This gives a date serial with '0' on 1 Jan 1970. my $serial = $time / 86400; # Adjust the date serial by the offset appropriate to the # currently selected system (1900/1904). if ($DATE_SYSTEM == 0) { # use 1900 system $serial += 25569; } else { # use 1904 system $serial += 24107; } # Actually, this gives the wrong date. Maybe this is specific to # gnumeric and OpenOffice on UNIX. # The correct date appears if we don't correct the value for time zone. # # Fix for timezone # my $offset = tz_local_offset( $time ); # $serial += $offset / 8640; # Perpetuate the error that 1900 was a leap year by decrementing # the serial if we're using the 1900 system and the date is prior to # 1 Mar 1900. This has the effect of making serial value '60' # 29 Feb 1900. # This fix only has any effect if UNIX/Perl time on the platform # can represent 1900. Many can't. unless ($DATE_SYSTEM) { $serial-- if ($serial < 61); # '61' is 1 Mar 1900 } return $serial; } sub new { my ( $self, %args ) = @_; $self = $self->SUPER::new( %args, 'summary_when' => 'after', ); $self->{'_output_file'} = $args{'output_file'} || '-', return $self; } sub report_start { my $self = shift; $self->SUPER::report_start( @_ ); $self->{'report_date'} = undef; $self->{'report_timespan'} = undef; $self->{'workbook'} = new Spreadsheet::WriteExcel($self->{'_output_file'}); # Create some format that we are going to use $self->{'title_fmt'} = $self->{'workbook'}->addformat( 'size' => 18, 'bold' => 1, ); $self->{'date_fmt'} = $self->{'workbook'}->addformat( 'num_format' => "yyyy/mm/dd", 'align' => "left", 'valign' => "top", ); $self->{'timestamp_fmt'} = $self->{'workbook'}->addformat( 'num_format' => "yyyy/mm/dd hh:mm:ss", 'align' => "left", 'valign' => "top", ); $self->{'datetime_fmt'} = $self->{'workbook'}->addformat( 'num_format' => "yyyy/mm/dd hh:mm", 'align' => "left", 'valign' => "top", ); $self->{'time_fmt'} = $self->{'workbook'}->addformat( 'num_format' => "hh:mm", 'align' => "left", 'valign' => "top", ); $self->{'name_fmt'} = $self->{'workbook'}->addformat( 'num_format' => 0x31, # Builtin: @ 'align' => "justify", 'valign' => "top", ); $self->{'label_fmt'} = $self->{'workbook'}->addformat( 'num_format' => 0x31, # Builtin: @ 'align' => "left", 'valign' => "left", 'bold' => 1, 'size' => 12, ); $self->{'header_fmt'} = $self->{'workbook'}->addformat( 'num_format' => 0x31, # Builtin: @ 'align' => "left", 'valign' => "left", 'bold' => 1, ); $self->{'description_fmt'} = $self->{'workbook'}->addformat( 'num_format' => 0x31, # Builtin: @ 'align' => "left", 'valign' => "left", ); $self->{'value_fmt'} = $self->{'workbook'}->addformat( 'num_format' => 0x00, # Builtin: General 'align' => "right", 'valign' => "top", ); } sub report_end { my $self = shift; $self->SUPER::report_end( @_ ); # Activate the first sheet my $first = ($self->{'workbook'}->sheets)[0]; if ( $first ) { $first->set_first_sheet(); $first->activate(); } $self->{'workbook'}->close(); return; } sub section_end { my $self = shift; $self->{'curr_section'}->write( $self->{'curr_row'}++, 0, "No subreports were generated for this section.", $self->{'label_fmt'}, ) if ( ! $self->current_section_subreport_count() ); $self->SUPER::section_end( @_ ); return; } sub handle_title { my ( $self, $title ) = @_; # Trim the title $title =~ s/^\s*//; $title =~ s/\s*$//; if ($self->in_element( "lire:section" ) ) { # Section's title my $title = substr( $title, 0, MAX_SHEET_STRING ); my $first_sheet = ! $self->{'workbook'}->sheets; # Create a new worksheet $self->{'curr_section'} = $self->{'workbook'}->addworksheet( $title ); $self->{'curr_row'} = 0; $self->{'curr_cols_width'} = []; if ( $first_sheet ) { # Put "header" information on first sheet my @labels = ( "Report generated", "Log file started", "Log file ended" ); my @dates = ( $self->{'report_date'}, @{$self->{'report_timespan'}} ); for (my $i=0; $i<@labels; $i++) { my $date = epoch2excel_date( $dates[$i] ); $self->{'curr_section'}->write( $i, 0, $labels[$i], $self->{'label_fmt'}, ); $self->{'curr_section'}->write( $i, 1, $date, $self->{'timestamp_fmt'}, ); $self->{'curr_section'}->set_row( $i, 14 ); } $self->{'curr_row'} = @labels + 1; my $width = length("yyyy/mm/dd hh:mm:ss") * CHAR_WIDTH; $self->{'curr_cols_width'}[1] = $width; $self->{'curr_section'}->set_column( 1, 1, $width ); } } else { # Subreport's title my $title = substr( $title, 0, MAX_EXCEL_STRING ); $self->{'curr_section'}->set_row( $self->{'curr_row'}, 22 ); $self->{'curr_section'}->write( $self->{'curr_row'}++, 0, $title, $self->{'title_fmt'}, ); } return; } sub handle_description { my ( $self, $docbook ) = @_; my $desc = dbk2txt( $docbook ); # Put each line in a separate cell foreach my $line ( split /\n/, $desc ) { if ( defined $line ) { # Trim spaces $line =~ s/^\s+//; $line =~ s/\s+$//; $self->{'curr_section'}->write( $self->{'curr_row'}, 0, $line, $self->{'description_fmt'}, ); } $self->{'curr_row'}++; } # Skip one row after the description $self->{'curr_row'}++; return; } sub handle_date { my ( $self, $date, $time ) = @_; $self->{'report_date'} = $time unless defined $self->{'report_date'}; return; } sub handle_timespan { my ( $self, $timespan, $start, $end ) = @_; $self->{'report_timespan'} = [$start, $end] unless defined $self->{'report_timespan'}; return; } sub subreport_end { my $self = shift; $self->SUPER::subreport_end( @_ ); # Leave one extra empty row $self->{'curr_row'} += 2; return; } sub table_start { my $self = shift; $self->SUPER::table_start( @_ ); $self->{'curr_headers'} = []; return; } sub table_end { my $self = shift; # Skip one row $self->{'curr_row'}++; $self->{'curr_section'}->write( $self->{'curr_row'}++, 0, "No content in report.", $self->{'label_fmt'} ) if ! $self->current_table_entry_count(); $self->SUPER::table_end( @_ ); return; } sub table_info_end { my $self = shift; $self->SUPER::table_info_end( @_ ); # Upgrade the column's width of the sheet based # on the suggested column's width my $table_info = $self->current_table_info(); my @cols = $table_info->column_infos(); foreach my $col ( @cols ) { my $i = $col->col_start; my $curr_width = $self->{'curr_cols_width'}[$i] || DEFAULT_CELL_WIDTH; my $width = $col->col_width * CHAR_WIDTH; $width = MAX_CELL_WIDTH if $width > MAX_CELL_WIDTH; if ( $width > $curr_width ) { $self->{'curr_cols_width'}[$i] = $width; $self->{'curr_section'}->set_column( $i, $i, $width ); } } return; } sub handle_header_row { my ( $self, $row ) = @_; push @{$self->{'curr_headers'}}, $row; return; } sub write_header_rows { my $self = $_[0]; foreach my $row ( @{$self->{'curr_headers'}} ) { $self->{'curr_row'}++; $self->{'curr_row_height'} = DEFAULT_ROW_HEIGHT; foreach my $c ( @$row ) { next unless defined $c; my $s = substr $c->label, 0, MAX_EXCEL_STRING; $self->{'curr_section'}->write( $self->{'curr_row'}, $c->col_start, $s, $self->{'header_fmt'} ); } } return; } sub handle_row { my ( $self, $row ) = @_; if ( $self->{'curr_headers'} ) { $self->write_header_rows; delete $self->{'curr_headers'}; } $self->{'curr_row'}++; $self->{'curr_row_height'} = DEFAULT_ROW_HEIGHT; foreach my $c ( @$row ) { next unless defined $c; if ($c->{'col_info'}->class eq 'categorical' ) { $self->write_categorical_cell( $c ); } else { $self->write_value_cell( $c ); } } return; } sub handle_table_summary { my ( $self, $nrecords, $row ) = @_; return unless $nrecords && $self->current_table_entry_count(); $self->{'curr_row_height'} = DEFAULT_ROW_HEIGHT; # Write the summary cell $self->{'curr_section'}->write( $self->{'curr_row'}, 0, "Total", $self->{'header_fmt'}, ); foreach my $c ( @$row ) { next unless defined $c; $self->write_value_cell( $c ); } # We increase it only after because it was already increased # in table_end $self->{'curr_row'}++; return; } sub write_categorical_cell { my ( $self, $name ) = @_; my $s = substr $name->{'content'}, 0, MAX_EXCEL_STRING; my $fmt = $self->{'name_fmt'}; # Convert time classes to their Excel representation if ( $name->{'value'} =~ /^\d+$/ && $name->{'content'} !~ /^(\[|Week)/ && #! Skip Week and rangegroup defined $name->{'range'} && $name->{'range'} =~ /^\d+$/ ) { $s = epoch2excel_date( $name->{'value'} ); if ( $name->{'range'} < 86400 ) { $fmt = $self->{'datetime_fmt'}; } else { $fmt = $self->{'date_fmt'}; } } my $col = $name->{'col_info'}->col_start; my $len = length $s; # Enlarge the row's height i if ( $len * CHAR_WIDTH > MAX_CELL_WIDTH ) { my $row = ceil( $len * CHAR_WIDTH / MAX_CELL_WIDTH ); my $height = $row * DEFAULT_ROW_HEIGHT; if ( $height > $self->{'curr_row_height'} ) { $self->{'curr_section'}->set_row( $self->{'curr_row'}, $height, ); $self->{'curr_row_height'} = $height; } } # Write it $self->{'curr_section'}->write( $self->{'curr_row'}, $col, $s, $fmt ); return; } sub write_value_cell { my ( $self, $value ) = @_; $self->{'curr_section'}->write( $self->{'curr_row'}, $value->{'col_info'}->col_start, $value->{'content'}, $self->{'value_fmt'} ); return; } 1; __END__ =pod =head1 NAME Lire::ReportParser::ExcelWriter - Transform Lire XML Reports into a Excel95(tm) spreadsheet. =head1 SYNOPSIS use Lire::ReportParser::ExceltWriter; my $parser = new Lire::ReportParser::ExcelWriter(); eval { $parser->parsefile( "report.xml" ) }; print "Parse failed: $@" if $@; =head1 DESCRIPTION This is a Lire::ReportParser processor which will transform the Lire XML report into a Excel95(tm) spreadsheet which will be output on STDOUT. Its constructor doesn't take any parameters. =head1 SEE ALSO Lire::ReportParser(3pm) Spreadsheet::WriteExcel(3pm) =head1 VERSION $Id: ExcelWriter.pm,v 1.19 2006/07/23 13:16:31 vanbaal Exp $ =head1 COPYRIGHT Copyright (C) 2001-2002 Stichting LogReport Foundation LogReport@LogReport.org This file is part of Lire. Lire is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program (see COPYING); if not, check with http://www.gnu.org/copyleft/gpl.html. =head1 AUTHOR Francis J. Lacoste =cut