diff --git a/lib/Excel/Writer/XLSX/Utility.pm b/lib/Excel/Writer/XLSX/Utility.pm index fd5eb60d..a1582328 100644 --- a/lib/Excel/Writer/XLSX/Utility.pm +++ b/lib/Excel/Writer/XLSX/Utility.pm @@ -218,23 +218,97 @@ sub xl_range_formula { # # Sheetnames used in references should be quoted if they contain any spaces, # special characters or if they look like something that isn't a sheet name. +# The rules are shown inline below. # sub quote_sheetname { - my $sheetname = $_[0]; + my $sheetname = shift; + my $uc_sheetname = uc( $sheetname ); + my $needs_quoting = 0; + my $row_max = 1_048_576; + my $col_max = 16_384; - # Use Excel's conventions and quote the sheet name if it contains any - # non-word character or if it isn't already quoted. - if ( $sheetname =~ /\W/ && $sheetname !~ /^'/ ) { + # Don't quote sheetname if it is already quoted by the user. + if ( $sheetname !~ /^'/ ) { + + + # Rule 1. Sheet names that contain anything other than \w and "." + # characters must be quoted. + if ( $sheetname =~ /[^\w\.\p{Emoticons}]/ ) { + $needs_quoting = 1; + } + + # Rule 2. Sheet names that start with a digit or "." must be quoted. + elsif ( $sheetname =~ /^[\d\.\p{Emoticons}]/ ) { + $needs_quoting = 1; + } + + # Rule 3. Sheet names must not be a valid A1 style cell reference. + # Valid means that the row and column values are within Excel limits. + elsif ( $uc_sheetname =~ /^([A-Z]{1,3}\d+)$/ ) { + my ( $row, $col ) = xl_cell_to_rowcol( $1 ); + + if ( $row >= 0 && $row < $row_max && $col >= 0 && $col < $col_max ) + { + $needs_quoting = 1; + } + } + + # Rule 4. Sheet names must not *start* with a valid RC style cell + # reference. Valid means that the row and column values are within + # Excel limits. + + # Rule 4a. Check for some single R/C references. + elsif ($uc_sheetname eq "R" + || $uc_sheetname eq "C" + || $uc_sheetname eq "RC" ) + { + $needs_quoting = 1; + + } + + # Rule 4b. Check for C1 or RC1 style references. References without + # trailing characters (like C12345) are caught by Rule 3. + elsif ( $uc_sheetname =~ /^R?C(\d+)/ ) { + my $col = $1; + if ( $col > 0 && $col <= $col_max ) { + $needs_quoting = 1; + } + } + + # Rule 4c. Check for R1C1 style references where both the number + # ranges are optional. Note that only 1 of the number ranges is + # required to be valid. + elsif ( $uc_sheetname =~ /^R(\d+)?C(\d+)?/ ) { + if ( defined $1 ) { + my $row = $1; + if ( $row > 0 && $row <= $row_max ) { + $needs_quoting = 1; + } + } + + if ( defined $2 ) { + my $col = $1; + if ( $col > 0 && $col <= $col_max ) { + $needs_quoting = 1; + } + } + } + } + + + if ( $needs_quoting ) { # Double quote any single quotes. $sheetname =~ s/'/''/g; $sheetname = q(') . $sheetname . q('); } + return $sheetname; } + ############################################################################### # # xl_inc_row($string) diff --git a/t/regression/quote_name08.t b/t/regression/quote_name08.t new file mode 100644 index 00000000..7d2237b0 --- /dev/null +++ b/t/regression/quote_name08.t @@ -0,0 +1,91 @@ +############################################################################### +# +# Tests the output of Excel::Writer::XLSX against Excel generated files. +# +# Copyright 2000-2024, John McNamara, jmcnamara@cpan.org +# +# SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later +# + +use lib 't/lib'; +use TestFunctions qw(_compare_xlsx_files _is_deep_diff); +use strict; +use warnings; + +use Test::More tests => 1; + +############################################################################### +# +# Tests setup. +# +my $filename = 'quote_name08.xlsx'; +my $dir = 't/regression/'; +my $got_filename = $dir . "ewx_$filename"; +my $exp_filename = $dir . 'xlsx_files/' . $filename; + +my $ignore_members = []; + +my $ignore_elements = {}; + + +############################################################################### +# +# Test the creation of a simple Excel::Writer::XLSX file. +# +use Excel::Writer::XLSX; + +my $workbook = Excel::Writer::XLSX->new( $got_filename ); +my $worksheet = $workbook->add_worksheet('1Sheet'); +my $chart = $workbook->add_chart( type => 'column', embedded => 1 ); + +# For testing, copy the randomly generated axis ids in the target xlsx file. +$chart->{_axis_ids} = [ 55487104, 84573184 ]; + +my $data = [ + [ 1, 2, 3, 4, 5 ], + [ 2, 4, 6, 8, 10 ], + [ 3, 6, 9, 12, 15 ], + +]; + +$worksheet->write( 'A1', $data ); +$worksheet->repeat_rows( 0, 1 ); +$worksheet->set_portrait(); +$worksheet->{_vertical_dpi} = 200; + +$chart->add_series( values => [ '1Sheet', 0, 4, 0, 0 ] ); +$chart->add_series( values => [ '1Sheet', 0, 4, 1, 1 ] ); +$chart->add_series( values => [ '1Sheet', 0, 4, 2, 2 ] ); + +$worksheet->insert_chart( 'E9', $chart ); + +$workbook->close(); + + +############################################################################### +# +# Compare the generated and existing Excel files. +# + +my ( $got, $expected, $caption ) = _compare_xlsx_files( + + $got_filename, + $exp_filename, + $ignore_members, + $ignore_elements, +); + +_is_deep_diff( $got, $expected, $caption ); + + + +############################################################################### +# +# Cleanup. +# +unlink $got_filename; + +__END__ + + + diff --git a/t/regression/quote_name09.t b/t/regression/quote_name09.t new file mode 100644 index 00000000..04667b57 --- /dev/null +++ b/t/regression/quote_name09.t @@ -0,0 +1,91 @@ +############################################################################### +# +# Tests the output of Excel::Writer::XLSX against Excel generated files. +# +# Copyright 2000-2024, John McNamara, jmcnamara@cpan.org +# +# SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later +# + +use lib 't/lib'; +use TestFunctions qw(_compare_xlsx_files _is_deep_diff); +use strict; +use warnings; + +use Test::More tests => 1; + +############################################################################### +# +# Tests setup. +# +my $filename = 'quote_name09.xlsx'; +my $dir = 't/regression/'; +my $got_filename = $dir . "ewx_$filename"; +my $exp_filename = $dir . 'xlsx_files/' . $filename; + +my $ignore_members = []; + +my $ignore_elements = {}; + + +############################################################################### +# +# Test the creation of a simple Excel::Writer::XLSX file. +# +use Excel::Writer::XLSX; + +my $workbook = Excel::Writer::XLSX->new( $got_filename ); +my $worksheet = $workbook->add_worksheet('Sheet_1'); +my $chart = $workbook->add_chart( type => 'column', embedded => 1 ); + +# For testing, copy the randomly generated axis ids in the target xlsx file. +$chart->{_axis_ids} = [ 54437760, 59195776 ]; + +my $data = [ + [ 1, 2, 3, 4, 5 ], + [ 2, 4, 6, 8, 10 ], + [ 3, 6, 9, 12, 15 ], + +]; + +$worksheet->write( 'A1', $data ); +$worksheet->repeat_rows( 0, 1 ); +$worksheet->set_portrait(); +$worksheet->{_vertical_dpi} = 200; + +$chart->add_series( values => [ 'Sheet_1', 0, 4, 0, 0 ] ); +$chart->add_series( values => [ 'Sheet_1', 0, 4, 1, 1 ] ); +$chart->add_series( values => [ 'Sheet_1', 0, 4, 2, 2 ] ); + +$worksheet->insert_chart( 'E9', $chart ); + +$workbook->close(); + + +############################################################################### +# +# Compare the generated and existing Excel files. +# + +my ( $got, $expected, $caption ) = _compare_xlsx_files( + + $got_filename, + $exp_filename, + $ignore_members, + $ignore_elements, +); + +_is_deep_diff( $got, $expected, $caption ); + + + +############################################################################### +# +# Cleanup. +# +unlink $got_filename; + +__END__ + + + diff --git a/t/regression/quote_name10.t b/t/regression/quote_name10.t new file mode 100644 index 00000000..ac32f27b --- /dev/null +++ b/t/regression/quote_name10.t @@ -0,0 +1,91 @@ +############################################################################### +# +# Tests the output of Excel::Writer::XLSX against Excel generated files. +# +# Copyright 2000-2024, John McNamara, jmcnamara@cpan.org +# +# SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later +# + +use lib 't/lib'; +use TestFunctions qw(_compare_xlsx_files _is_deep_diff); +use strict; +use warnings; + +use Test::More tests => 1; + +############################################################################### +# +# Tests setup. +# +my $filename = 'quote_name10.xlsx'; +my $dir = 't/regression/'; +my $got_filename = $dir . "ewx_$filename"; +my $exp_filename = $dir . 'xlsx_files/' . $filename; + +my $ignore_members = []; + +my $ignore_elements = {}; + + +############################################################################### +# +# Test the creation of a simple Excel::Writer::XLSX file. +# +use Excel::Writer::XLSX; + +my $workbook = Excel::Writer::XLSX->new( $got_filename ); +my $worksheet = $workbook->add_worksheet('Sh.eet.1'); +my $chart = $workbook->add_chart( type => 'column', embedded => 1 ); + +# For testing, copy the randomly generated axis ids in the target xlsx file. +$chart->{_axis_ids} = [ 46905600, 46796800 ]; + +my $data = [ + [ 1, 2, 3, 4, 5 ], + [ 2, 4, 6, 8, 10 ], + [ 3, 6, 9, 12, 15 ], + +]; + +$worksheet->write( 'A1', $data ); +$worksheet->repeat_rows( 0, 1 ); +$worksheet->set_portrait(); +$worksheet->{_vertical_dpi} = 200; + +$chart->add_series( values => [ 'Sh.eet.1', 0, 4, 0, 0 ] ); +$chart->add_series( values => [ 'Sh.eet.1', 0, 4, 1, 1 ] ); +$chart->add_series( values => [ 'Sh.eet.1', 0, 4, 2, 2 ] ); + +$worksheet->insert_chart( 'E9', $chart ); + +$workbook->close(); + + +############################################################################### +# +# Compare the generated and existing Excel files. +# + +my ( $got, $expected, $caption ) = _compare_xlsx_files( + + $got_filename, + $exp_filename, + $ignore_members, + $ignore_elements, +); + +_is_deep_diff( $got, $expected, $caption ); + + + +############################################################################### +# +# Cleanup. +# +unlink $got_filename; + +__END__ + + + diff --git a/t/regression/quote_name11.t b/t/regression/quote_name11.t new file mode 100644 index 00000000..83ad1741 --- /dev/null +++ b/t/regression/quote_name11.t @@ -0,0 +1,92 @@ +############################################################################### +# +# Tests the output of Excel::Writer::XLSX against Excel generated files. +# +# Copyright 2000-2024, John McNamara, jmcnamara@cpan.org +# +# SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later +# + +use lib 't/lib'; +use TestFunctions qw(_compare_xlsx_files _is_deep_diff); +use strict; +use warnings; +use utf8; + +use Test::More tests => 1; + +############################################################################### +# +# Tests setup. +# +my $filename = 'quote_name11.xlsx'; +my $dir = 't/regression/'; +my $got_filename = $dir . "ewx_$filename"; +my $exp_filename = $dir . 'xlsx_files/' . $filename; + +my $ignore_members = []; + +my $ignore_elements = {}; + + +############################################################################### +# +# Test the creation of a simple Excel::Writer::XLSX file. +# +use Excel::Writer::XLSX; + +my $workbook = Excel::Writer::XLSX->new( $got_filename ); +my $worksheet = $workbook->add_worksheet('Sheeté'); +my $chart = $workbook->add_chart( type => 'column', embedded => 1 ); + +# For testing, copy the randomly generated axis ids in the target xlsx file. +$chart->{_axis_ids} = [ 46720128, 46721664 ]; + +my $data = [ + [ 1, 2, 3, 4, 5 ], + [ 2, 4, 6, 8, 10 ], + [ 3, 6, 9, 12, 15 ], + +]; + +$worksheet->write( 'A1', $data ); +$worksheet->repeat_rows( 0, 1 ); +$worksheet->set_portrait(); +$worksheet->{_vertical_dpi} = 200; + +$chart->add_series( values => [ 'Sheeté', 0, 4, 0, 0 ] ); +$chart->add_series( values => [ 'Sheeté', 0, 4, 1, 1 ] ); +$chart->add_series( values => [ 'Sheeté', 0, 4, 2, 2 ] ); + +$worksheet->insert_chart( 'E9', $chart ); + +$workbook->close(); + + +############################################################################### +# +# Compare the generated and existing Excel files. +# + +my ( $got, $expected, $caption ) = _compare_xlsx_files( + + $got_filename, + $exp_filename, + $ignore_members, + $ignore_elements, +); + +_is_deep_diff( $got, $expected, $caption ); + + + +############################################################################### +# +# Cleanup. +# +unlink $got_filename; + +__END__ + + + diff --git a/t/regression/xlsx_files/quote_name08.xlsx b/t/regression/xlsx_files/quote_name08.xlsx new file mode 100644 index 00000000..da4328d1 Binary files /dev/null and b/t/regression/xlsx_files/quote_name08.xlsx differ diff --git a/t/regression/xlsx_files/quote_name09.xlsx b/t/regression/xlsx_files/quote_name09.xlsx new file mode 100644 index 00000000..a0310b43 Binary files /dev/null and b/t/regression/xlsx_files/quote_name09.xlsx differ diff --git a/t/regression/xlsx_files/quote_name10.xlsx b/t/regression/xlsx_files/quote_name10.xlsx new file mode 100644 index 00000000..7edfe26a Binary files /dev/null and b/t/regression/xlsx_files/quote_name10.xlsx differ diff --git a/t/regression/xlsx_files/quote_name11.xlsx b/t/regression/xlsx_files/quote_name11.xlsx new file mode 100644 index 00000000..63dc71bf Binary files /dev/null and b/t/regression/xlsx_files/quote_name11.xlsx differ diff --git a/t/utility/quote_sheetname.t b/t/utility/quote_sheetname.t new file mode 100644 index 00000000..05749218 --- /dev/null +++ b/t/utility/quote_sheetname.t @@ -0,0 +1,186 @@ +############################################################################### +# +# Tests for Excel::Writer::XLSX::Utility. +# +# Copyright 2000-2024, John McNamara, jmcnamara@cpan.org +# +# SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later +# + +use utf8; +use strict; +use warnings; +use Excel::Writer::XLSX::Utility qw(quote_sheetname); + +use Test::More tests => 100; + +############################################################################### +# +# Tests setup. +# +my $got; +my $expected; +my $caption; + +# The following unquoted and quoted sheet names were extracted from Excel +# files. +my @test_cases = ( + + # A sheetname that is already quoted. + [ "'Sheet 1'", "'Sheet 1'" ], + + # ---------------------------------------------------------------- + # Rule 1. + # ---------------------------------------------------------------- + + # Some simple variants on standard sheet names. + [ "Sheet1", "Sheet1" ], + [ "Sheet.1", "Sheet.1" ], + [ "Sheet_1", "Sheet_1" ], + [ "Sheet-1", "'Sheet-1'" ], + [ "Sheet 1", "'Sheet 1'" ], + [ "Sheet#1", "'Sheet#1'" ], + [ "#Sheet1", "'#Sheet1'" ], + + # Sheetnames with single quotes. + [ "Sheet'1", "'Sheet''1'" ], + [ "Sheet''1", "'Sheet''''1'" ], + + # Single special chars that are unquoted in sheetnames. These are + # variants of the first char rule. + [ "_", "_" ], + [ ".", "'.'" ], + + # White space only. + [ " ", "' '" ], + [ " ", "' '" ], + + # Sheetnames with unicode or emojis. + [ "été", "été" ], + [ "mangé", "mangé" ], + [ "Sheet😀", "Sheet😀" ], + [ "Sheet⟦1", "'Sheet⟦1'" ], # Unicode punctuation. + [ "Sheet᠅1", "'Sheet᠅1'" ], # Unicode punctuation. + + # ---------------------------------------------------------------- + # Rule 2. + # ---------------------------------------------------------------- + + # Sheetnames starting with non-word characters. + [ "_Sheet1", "_Sheet1" ], + [ ".Sheet1", "'.Sheet1'" ], + [ "1Sheet1", "'1Sheet1'" ], + [ "-Sheet1", "'-Sheet1'" ], + [ "😀Sheet", "'😀Sheet'" ], + + # Sheetnames that are digits only also start with a non word char. + [ "1", "'1'" ], + [ "2", "'2'" ], + [ "1234", "'1234'" ], + [ "12345678", "'12345678'" ], + + # ---------------------------------------------------------------- + # Rule 3. + # ---------------------------------------------------------------- + + # Worksheet names that look like A1 style references [ with the + # row/column number in the Excel allowable range ]. These are case + # insensitive. + [ "A0", "A0" ], + [ "A1", "'A1'" ], + [ "a1", "'a1'" ], + [ "XFD", "XFD" ], + [ "xfd", "xfd" ], + [ "XFE1", "XFE1" ], + [ "ZZZ1", "ZZZ1" ], + [ "XFD1", "'XFD1'" ], + [ "xfd1", "'xfd1'" ], + [ "B1048577", "B1048577" ], + [ "A1048577", "A1048577" ], + [ "A1048576", "'A1048576'" ], + [ "B1048576", "'B1048576'" ], + [ "B1048576a", "B1048576a" ], + [ "XFD048576", "'XFD048576'" ], + [ "XFD1048576", "'XFD1048576'" ], + [ "XFD01048577", "XFD01048577" ], + [ "XFD01048576", "'XFD01048576'" ], + [ "A123456789012345678901", "A123456789012345678901" ], # Exceeds u64. + + # ---------------------------------------------------------------- + # Rule 4. + # ---------------------------------------------------------------- + + # Sheet names that *start* with RC style references [ with the + # row/column number in the Excel allowable range ]. These are case + # insensitive. + [ "A", "A" ], + [ "B", "B" ], + [ "D", "D" ], + [ "Q", "Q" ], + [ "S", "S" ], + [ "c", "'c'" ], + [ "C", "'C'" ], + [ "CR", "CR" ], + [ "CZ", "CZ" ], + [ "r", "'r'" ], + [ "R", "'R'" ], + [ "C8", "'C8'" ], + [ "rc", "'rc'" ], + [ "RC", "'RC'" ], + [ "RCZ", "RCZ" ], + [ "RRC", "RRC" ], + [ "R0C0", "R0C0" ], + [ "R4C", "'R4C'" ], + [ "R5C", "'R5C'" ], + [ "rc2", "'rc2'" ], + [ "RC2", "'RC2'" ], + [ "RC8", "'RC8'" ], + [ "bR1C1", "bR1C1" ], + [ "R1C1", "'R1C1'" ], + [ "r1c2", "'r1c2'" ], + [ "rc2z", "'rc2z'" ], + [ "bR1C1b", "bR1C1b" ], + [ "R1C1b", "'R1C1b'" ], + [ "R1C1R", "'R1C1R'" ], + [ "C16384", "'C16384'" ], + [ "C16385", "'C16385'" ], + [ "C16385Z", "C16385Z" ], + [ "C16386", "'C16386'" ], + [ "C16384Z", "'C16384Z'" ], + [ "PC16384Z", "PC16384Z" ], + [ "RC16383", "'RC16383'" ], + [ "RC16385Z", "RC16385Z" ], + [ "R1048576", "'R1048576'" ], + [ "R1048577C", "R1048577C" ], + [ "R1C16384", "'R1C16384'" ], + [ "R1C16385", "'R1C16385'" ], + [ "RC16384Z", "'RC16384Z'" ], + [ "R1048576C", "'R1048576C'" ], + [ "R1048577C1", "R1048577C1" ], + [ "R1C16384Z", "'R1C16384Z'" ], + [ "R1048575C1", "'R1048575C1'" ], + [ "R1048576C1", "'R1048576C1'" ], + [ "R1048577C16384", "R1048577C16384" ], + [ "R1048576C16384", "'R1048576C16384'" ], + [ "R1048576C16385", "'R1048576C16385'" ], + [ "ZR1048576C16384", "ZR1048576C16384" ], + [ "C123456789012345678901Z", "C123456789012345678901Z" ], # Exceeds u64. + [ "R123456789012345678901Z", "R123456789012345678901Z" ], # Exceeds u64. +); + + +############################################################################### +# +# Test the xl_rowcol_to_cell method. +# +$caption = " \tUtility: quote_sheetname()"; + +for my $aref ( @test_cases ) { + my $got = quote_sheetname( $aref->[0] ); + my $exp = $aref->[1]; + + is( $got, $exp, $caption ); +} + + +__END__