diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dcc0a3..0ba66cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.6.0 (2024-07-10) + +- Move to [ZipKit gem](http://rubygems.org/gems/zip_kit); equivalent of https://github.com/felixbuenemann/xlsxtream/pull/57/files + +## 2.5.0 (2021-06-28) + +- New `:add_header_row` method in Worksheet, which outputs as a row as bold text, intended for header rows + ## 2.4.0 (2020-06-27) - Allow writing worksheets without a block using add\_worksheet (#42, #45) @@ -23,11 +31,11 @@ ## 2.0.1 (2018-03-11) - Rescue gracefully from invalid dates with auto-format (#22) -- Remove unused ZipTricksFibers IO wrapper (#24) +- Remove unused ZipKitFibers IO wrapper (#24) ## 2.0.0 (2017-10-31) -- Replace RubyZip with ZipTricks as default compressor (#16) +- Replace RubyZip with ZipKit as default compressor (#16) - Drop support for Ruby < 2.1.0 (required for zip\_tricks gem) - Deprecate :io\_wrapper option, you can now pass wrapper instances (#20) diff --git a/README.md b/README.md index 5475554..05ac496 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,12 @@ xlsx.write_worksheet 'AppendixSheet' do |sheet| sheet.add_row [Time.now, 'Time-machine'] end +# Output the first row as a header line using bold text +xls.write_worksheet 'Sheet1' do |sheet| + sheet.add_header_row ['headers', 'in', 'bold'] + sheet << ['first', 'normal', 'row'] +end + # If you have highly repetitive data, you can enable Shared String Tables (SST) # for the workbook or a single worksheet. The SST has to be kept in memory, # so do not use it if you have a huge amount of rows or a little duplication diff --git a/lib/xlsxtream/header_row.rb b/lib/xlsxtream/header_row.rb new file mode 100644 index 0000000..6166420 --- /dev/null +++ b/lib/xlsxtream/header_row.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Xlsxtream + class HeaderRow < Row + def initialize(row, rownum, options = {}) + super + + @normal_style = ' s="3"' + @date_style = ' s="4"' + @time_style = ' s="5"' + end + end +end diff --git a/lib/xlsxtream/io/zip_kit.rb b/lib/xlsxtream/io/zip_kit.rb new file mode 100644 index 0000000..a7d6236 --- /dev/null +++ b/lib/xlsxtream/io/zip_kit.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require "zip_kit" + +module Xlsxtream + module IO + class ZipKit + BUFFER_SIZE = 64 * 1024 + + def initialize(body) + @streamer = ::ZipKit::Streamer.new(body) + @wf = nil + @buffer = String.new + end + + def <<(data) + @buffer << data + flush_buffer if @buffer.size >= BUFFER_SIZE + self + end + + def add_file(path) + flush_file + @wf = @streamer.write_deflated_file(path) + end + + def close + flush_file + @streamer.close + end + + private + + def flush_buffer + @wf << @buffer + @buffer.clear + end + + def flush_file + return unless @wf + flush_buffer if @buffer.size > 0 + @wf.close + end + end + end +end diff --git a/lib/xlsxtream/io/zip_tricks.rb b/lib/xlsxtream/io/zip_tricks.rb index 3bf57d8..a7d6236 100644 --- a/lib/xlsxtream/io/zip_tricks.rb +++ b/lib/xlsxtream/io/zip_tricks.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require "zip_tricks" +require "zip_kit" module Xlsxtream module IO - class ZipTricks + class ZipKit BUFFER_SIZE = 64 * 1024 def initialize(body) - @streamer = ::ZipTricks::Streamer.new(body) + @streamer = ::ZipKit::Streamer.new(body) @wf = nil @buffer = String.new end diff --git a/lib/xlsxtream/row.rb b/lib/xlsxtream/row.rb index afba661..50d44dd 100644 --- a/lib/xlsxtream/row.rb +++ b/lib/xlsxtream/row.rb @@ -16,14 +16,15 @@ class Row TRUE_STRING = 'true'.freeze FALSE_STRING = 'false'.freeze - DATE_STYLE = 1 - TIME_STYLE = 2 - def initialize(row, rownum, options = {}) @row = row @rownum = rownum @sst = options[:sst] @auto_format = options[:auto_format] + + @normal_style = '' + @date_style = ' s="1"' + @time_style = ' s="2"' end def to_xml @@ -40,15 +41,15 @@ def to_xml case value when Numeric - xml << %Q{#{value}} + xml << %Q{#{value}} when TrueClass, FalseClass - xml << %Q{#{value ? 1 : 0}} + xml << %Q{#{value ? 1 : 0}} when Time - xml << %Q{#{time_to_oa_date(value)}} + xml << %Q{#{time_to_oa_date(value)}} when DateTime - xml << %Q{#{datetime_to_oa_date(value)}} + xml << %Q{#{datetime_to_oa_date(value)}} when Date - xml << %Q{#{date_to_oa_date(value)}} + xml << %Q{#{date_to_oa_date(value)}} else value = value.to_s @@ -56,9 +57,9 @@ def to_xml value = value.encode(ENCODING) if value.encoding != ENCODING if @sst - xml << %Q{#{@sst[value]}} + xml << %Q{#{@sst[value]}} else - xml << %Q{#{XML.escape_value(value)}} + xml << %Q{#{XML.escape_value(value)}} end end end diff --git a/lib/xlsxtream/workbook.rb b/lib/xlsxtream/workbook.rb index 1fdb04b..aabcb0e 100644 --- a/lib/xlsxtream/workbook.rb +++ b/lib/xlsxtream/workbook.rb @@ -3,7 +3,7 @@ require "xlsxtream/xml" require "xlsxtream/shared_string_table" require "xlsxtream/worksheet" -require "xlsxtream/io/zip_tricks" +require "xlsxtream/io/zip_kit" module Xlsxtream class Workbook @@ -46,13 +46,13 @@ def initialize(output, options = {}) end if output.is_a?(String) || !output.respond_to?(:<<) @file = File.open(output, 'wb') - @io = IO::ZipTricks.new(@file) + @io = IO::ZipKit.new(@file) elsif output.respond_to? :add_file @file = nil @io = output else @file = nil - @io = IO::ZipTricks.new(output) + @io = IO::ZipKit.new(output) end @sst = SharedStringTable.new @worksheets = [] @@ -162,12 +162,18 @@ def write_styles - + + + + + + + @@ -183,10 +189,19 @@ def write_styles - + + + + + + + + + + diff --git a/lib/xlsxtream/worksheet.rb b/lib/xlsxtream/worksheet.rb index c4e8b41..9f10881 100644 --- a/lib/xlsxtream/worksheet.rb +++ b/lib/xlsxtream/worksheet.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "xlsxtream/xml" require "xlsxtream/row" +require "xlsxtream/header_row" module Xlsxtream class Worksheet @@ -19,6 +20,11 @@ def <<(row) end alias_method :add_row, :<< + def add_header_row(row) + @io << HeaderRow.new(row, @rownum, @options).to_xml + @rownum += 1 + end + def close write_footer @closed = true diff --git a/test/xlsxtream/header_row_test.rb b/test/xlsxtream/header_row_test.rb new file mode 100644 index 0000000..6c7fb86 --- /dev/null +++ b/test/xlsxtream/header_row_test.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true +require 'test_helper' +require 'xlsxtream/header_row' + +module Xlsxtream + class HeaderRowTest < Minitest::Test + def test_header_string_column + row = HeaderRow.new(['hello'], 1) + expected = 'hello' + actual = row.to_xml + assert_equal expected, actual + end + + def test_header_symbol_column + row = HeaderRow.new([:hello], 1) + expected = 'hello' + actual = row.to_xml + assert_equal expected, actual + end + + def test_header_boolean_column + row = HeaderRow.new([true], 1) + actual = row.to_xml + expected = '1' + assert_equal expected, actual + row = HeaderRow.new([false], 1) + actual = row.to_xml + expected = '0' + assert_equal expected, actual + end + + def test_header_text_boolean_column + row = HeaderRow.new(['true'], 1, :auto_format => true) + actual = row.to_xml + expected = '1' + assert_equal expected, actual + row = HeaderRow.new(['false'], 1, :auto_format => true) + actual = row.to_xml + expected = '0' + assert_equal expected, actual + end + + def test_header_integer_column + row = HeaderRow.new([1], 1) + actual = row.to_xml + expected = '1' + assert_equal expected, actual + end + + def test_header_text_integer_column + row = HeaderRow.new(['1'], 1, :auto_format => true) + actual = row.to_xml + expected = '1' + assert_equal expected, actual + end + + def test_header_float_column + row = HeaderRow.new([1.5], 1) + actual = row.to_xml + expected = '1.5' + assert_equal expected, actual + end + + def test_header_text_float_column + row = HeaderRow.new(['1.5'], 1, :auto_format => true) + actual = row.to_xml + expected = '1.5' + assert_equal expected, actual + end + + def test_header_date_column + row = HeaderRow.new([Date.new(1900, 1, 1)], 1) + actual = row.to_xml + expected = '2.0' + assert_equal expected, actual + end + + def test_header_text_date_column + row = HeaderRow.new(['1900-01-01'], 1, :auto_format => true) + actual = row.to_xml + expected = '2.0' + assert_equal expected, actual + end + + def test_header_invalid_text_date_column + row = HeaderRow.new(['1900-02-29'], 1, :auto_format => true) + actual = row.to_xml + expected = '1900-02-29' + assert_equal expected, actual + end + + def test_header_date_time_column + row = HeaderRow.new([DateTime.new(1900, 1, 1, 12, 0, 0, '+00:00')], 1) + actual = row.to_xml + expected = '2.5' + assert_equal expected, actual + end + + def test_header_text_date_time_column + candidates = [ + '1900-01-01T12:00', + '1900-01-01T12:00Z', + '1900-01-01T12:00+00:00', + '1900-01-01T12:00:00+00:00', + '1900-01-01T12:00:00.000+00:00', + '1900-01-01T12:00:00.000000000Z' + ] + candidates.each do |timestamp| + row = HeaderRow.new([timestamp], 1, :auto_format => true) + actual = row.to_xml + expected = '2.5' + assert_equal expected, actual + end + row = HeaderRow.new(['1900-01-01T12'], 1, :auto_format => true) + actual = row.to_xml + expected = '2.5' + refute_equal expected, actual + end + + def test_header_invalid_text_date_time_column + row = HeaderRow.new(['1900-02-29T12:00'], 1, :auto_format => true) + actual = row.to_xml + expected = '1900-02-29T12:00' + assert_equal expected, actual + end + + def test_header_time_column + row = HeaderRow.new([Time.new(1900, 1, 1, 12, 0, 0, '+00:00')], 1) + actual = row.to_xml + expected = '2.5' + assert_equal expected, actual + end + + def test_header_string_column_with_shared_string_table + mock_sst = { 'hello' => 0 } + row = HeaderRow.new(['hello'], 1, :sst => mock_sst) + expected = '0' + actual = row.to_xml + assert_equal expected, actual + end + + def test_header_multiple_columns + row = HeaderRow.new(['foo', nil, 23], 1) + expected = 'foo23' + actual = row.to_xml + assert_equal expected, actual + end + end +end diff --git a/test/xlsxtream/io/zip_tricks_test.rb b/test/xlsxtream/io/zip_tricks_test.rb index 1aa63d3..3aab892 100644 --- a/test/xlsxtream/io/zip_tricks_test.rb +++ b/test/xlsxtream/io/zip_tricks_test.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true require 'test_helper' -require 'xlsxtream/io/zip_tricks' +require 'xlsxtream/io/zip_kit' require 'zip' module Xlsxtream - class ZipTricksTest < Minitest::Test + class ZipKitTest < Minitest::Test def test_writes_of_multiple_files zip_buf = Tempfile.new('ztio-test') - io = Xlsxtream::IO::ZipTricks.new(zip_buf) + io = Xlsxtream::IO::ZipKit.new(zip_buf) io.add_file("book1.xml") io << '' io.add_file("book2.xml") diff --git a/test/xlsxtream/workbook_test.rb b/test/xlsxtream/workbook_test.rb index a589cae..3516608 100644 --- a/test/xlsxtream/workbook_test.rb +++ b/test/xlsxtream/workbook_test.rb @@ -367,6 +367,29 @@ def test_add_columns_via_workbook_options_and_add_rows assert_equal expected, actual end + def test_add_header_row + iow_spy = io_wrapper_spy + Workbook.open(iow_spy) do |wb| + wb.write_worksheet(headers: ['foo']) do |ws| + ws << ['foo'] + ws.add_header_row ['bar'] # It's usually used as first row, but doesn't have to be + ws.add_row ['baz'] + end + end + + expected = \ + ''"\r\n" \ + '' \ + '' \ + 'foo' \ + 'bar' \ + 'baz' \ + '' + + actual = iow_spy['xl/worksheets/sheet1.xml'] + assert_equal expected, actual + end + def test_styles_content iow_spy = io_wrapper_spy Workbook.open(iow_spy) {} @@ -377,8 +400,14 @@ def test_styles_content '' \ '' \ '' \ - '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ '' \ + '' \ '' \ '' \ '' \ @@ -398,10 +427,19 @@ def test_styles_content '' \ '' \ '' \ - '' \ + '' \ '' \ '' \ '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ '' \ '' \ '' \ diff --git a/xlsxtream.gemspec b/xlsxtream.gemspec index 9f1149b..66e7301 100644 --- a/xlsxtream.gemspec +++ b/xlsxtream.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.1.0" - spec.add_dependency "zip_tricks", ">= 4.5", "< 6" + spec.add_dependency "zip_kit", ">= 6.0", "< 7" spec.add_development_dependency "bundler", ">= 1.7", "< 3" spec.add_development_dependency "rake"