diff --git a/CHANGELOG.md b/CHANGELOG.md index 94ff988..ab3b385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ +# 0.4.1 +Changes: +- `TableStructure::Iterator` +- `TableStructure::Writer` +- `TableStructure::CSV::Writer` + - Add `header: { step: n }` option. Header rows are output at intervals of step number. + - e.g. `TableStructure::Iterator.new(schema, header: { step: 10 })` + # 0.4.0 Changes: - Remove deprecated methods, arguments and options. -- It is recommended that you update to 0.3.23 first. if you get warnings or errors, fix them and then update to 0.4.0. +- It is recommended that you update to `0.3.23` first. If you get warnings or errors, fix them and then update to `0.4.0`. # 0.3.23 Changes: diff --git a/Gemfile.lock b/Gemfile.lock index 291c628..1d7cdd0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - table_structure (0.4.0) + table_structure (0.4.1) GEM remote: https://rubygems.org/ diff --git a/lib/table_structure.rb b/lib/table_structure.rb index 05e8bb7..e760bb2 100644 --- a/lib/table_structure.rb +++ b/lib/table_structure.rb @@ -30,7 +30,6 @@ class Error < StandardError; end require 'table_structure/table/column_converter' require 'table_structure/table/context_builder' require 'table_structure/table/row_builder' - require 'table_structure/table/iterator' require 'table_structure/writer' require 'table_structure/csv/writer' require 'table_structure/iterator' diff --git a/lib/table_structure/csv/writer.rb b/lib/table_structure/csv/writer.rb index 3491326..f6d2dfb 100644 --- a/lib/table_structure/csv/writer.rb +++ b/lib/table_structure/csv/writer.rb @@ -9,7 +9,7 @@ def initialize( schema, bom: false, csv_options: {}, - header: { context: nil } + header: { context: nil, step: nil } ) require 'csv' diff --git a/lib/table_structure/iterator.rb b/lib/table_structure/iterator.rb index 61f7af7..2074994 100644 --- a/lib/table_structure/iterator.rb +++ b/lib/table_structure/iterator.rb @@ -2,16 +2,26 @@ module TableStructure class Iterator + class HeaderOptions + attr_reader :enabled, :context, :step + alias enabled? enabled + + def initialize(options) + @enabled = !!options + if options.is_a?(Hash) + @context = options[:context] + @step = options[:step] + end + end + end + def initialize( schema, - header: { context: nil }, + header: { context: nil, step: nil }, row_type: :array ) - @schema = schema - @options = { - header: header, - row_type: row_type - } + @table = Table.new(schema, row_type: row_type) + @header_options = HeaderOptions.new(header) end def iterate(items, &block) @@ -19,22 +29,29 @@ def iterate(items, &block) raise ::TableStructure::Error, "Must be enumerable. #{items}" end - enum = - Table::Iterator - .new( - Table.new(@schema, row_type: @options[:row_type]), - header: @options[:header] - ) - .iterate(items) - - if block_given? - enum = - enum - .lazy - .map { |row| block.call(row) } + table_enum = ::Enumerator.new do |y| + body_enum = @table.body(items) + + if @header_options.enabled? + header_row = @table.header(context: @header_options.context) + y << header_row + + if @header_options.step&.positive? + loop do + @header_options.step.times { y << body_enum.next } + y << header_row + end + else + body_enum.each { |row| y << row } + end + else + body_enum.each { |row| y << row } + end end - enum + table_enum = table_enum.lazy.map(&block) if block_given? + + table_enum end end end diff --git a/lib/table_structure/table/iterator.rb b/lib/table_structure/table/iterator.rb deleted file mode 100644 index 114862b..0000000 --- a/lib/table_structure/table/iterator.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module TableStructure - class Table::Iterator - def initialize(table, header: { context: nil }) - @table = table - @header_options = header - end - - def iterate(items) - ::Enumerator.new do |y| - if @header_options - header_context = @header_options.is_a?(Hash) ? @header_options[:context] : nil - y << @table.header(context: header_context) - end - @table - .body(items) - .each { |row| y << row } - end - end - end -end diff --git a/lib/table_structure/version.rb b/lib/table_structure/version.rb index 7f00fc3..b73d85d 100644 --- a/lib/table_structure/version.rb +++ b/lib/table_structure/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module TableStructure - VERSION = '0.4.0' + VERSION = '0.4.1' end diff --git a/lib/table_structure/writer.rb b/lib/table_structure/writer.rb index 287a165..7586b91 100644 --- a/lib/table_structure/writer.rb +++ b/lib/table_structure/writer.rb @@ -4,7 +4,7 @@ module TableStructure class Writer def initialize( schema, - header: { context: nil }, + header: { context: nil, step: nil }, method: :<<, row_type: :array ) diff --git a/spec/support/shared_contexts.rb b/spec/support/shared_contexts.rb index 74206e9..6b200c1 100644 --- a/spec/support/shared_contexts.rb +++ b/spec/support/shared_contexts.rb @@ -42,3 +42,219 @@ ] end end + +RSpec.shared_context 'table_structured_array' do + let(:header_row) do + [ + 'ID', + 'Name', + 'Pet 1', + 'Pet 2', + 'Pet 3', + 'Q1', + 'Q2', + 'Q3' + ] + end + + let(:body_row_taro) do + [ + 1, + '太郎', + 'cat', + 'dog', + nil, + 'yes', + 'no', + 'yes' + ] + end + + let(:body_row_hanako) do + [ + 2, + '花子', + 'rabbit', + 'turtle', + 'squirrel', + 'yes', + 'yes', + 'no' + ] + end + + let(:body_row_jiro) do + [ + 3, + '次郎', + 'tiger', + 'elephant', + 'doragon', + 'no', + 'yes', + nil + ] + end +end + +RSpec.shared_context 'table_structured_array_with_stringified' do + let(:header_row) do + [ + 'ID', + 'Name', + 'Pet 1', + 'Pet 2', + 'Pet 3', + 'Q1', + 'Q2', + 'Q3' + ] + end + + let(:body_row_taro) do + [ + '1', + '太郎', + 'cat', + 'dog', + '', + 'yes', + 'no', + 'yes' + ] + end + + let(:body_row_hanako) do + %w[ + 2 + 花子 + rabbit + turtle + squirrel + yes + yes + no + ] + end + + let(:body_row_jiro) do + [ + '3', + '次郎', + 'tiger', + 'elephant', + 'doragon', + 'no', + 'yes', + '' + ] + end +end + +RSpec.shared_context 'table_structured_hash' do + let(:header_row) do + { + id: 'ID', + name: 'Name', + pet1: 'Pet 1', + pet2: 'Pet 2', + pet3: 'Pet 3', + q1: 'Q1', + q2: 'Q2', + q3: 'Q3' + } + end + + let(:body_row_taro) do + { + id: 1, + name: '太郎', + pet1: 'cat', + pet2: 'dog', + pet3: nil, + q1: 'yes', + q2: 'no', + q3: 'yes' + } + end + + let(:body_row_hanako) do + { + id: 2, + name: '花子', + pet1: 'rabbit', + pet2: 'turtle', + pet3: 'squirrel', + q1: 'yes', + q2: 'yes', + q3: 'no' + } + end + + let(:body_row_jiro) do + { + id: 3, + name: '次郎', + pet1: 'tiger', + pet2: 'elephant', + pet3: 'doragon', + q1: 'no', + q2: 'yes', + q3: nil + } + end +end + +RSpec.shared_context 'table_structured_hash_with_index_keys' do + let(:header_row) do + { + 0 => 'ID', + 1 => 'Name', + 2 => 'Pet 1', + 3 => 'Pet 2', + 4 => 'Pet 3', + 5 => 'Q1', + 6 => 'Q2', + 7 => 'Q3' + } + end + + let(:body_row_taro) do + { + 0 => 1, + 1 => '太郎', + 2 => 'cat', + 3 => 'dog', + 4 => nil, + 5 => 'yes', + 6 => 'no', + 7 => 'yes' + } + end + + let(:body_row_hanako) do + { + 0 => 2, + 1 => '花子', + 2 => 'rabbit', + 3 => 'turtle', + 4 => 'squirrel', + 5 => 'yes', + 6 => 'yes', + 7 => 'no' + } + end + + let(:body_row_jiro) do + { + 0 => 3, + 1 => '次郎', + 2 => 'tiger', + 3 => 'elephant', + 4 => 'doragon', + 5 => 'no', + 6 => 'yes', + 7 => nil + } + end +end diff --git a/spec/table_structure/csv/writer_spec.rb b/spec/table_structure/csv/writer_spec.rb index bef1cf5..65d0a5f 100644 --- a/spec/table_structure/csv/writer_spec.rb +++ b/spec/table_structure/csv/writer_spec.rb @@ -6,7 +6,7 @@ end let(:inner_writer_options) do - { header: { context: {} } } + { header: { context: {}, step: 10 } } end let(:csv_writer) do @@ -28,7 +28,7 @@ writer = double('TableStructure::Writer') expect(TableStructure::Writer).to receive(:new) - .with(schema, header: { context: {} }) + .with(schema, header: { context: {}, step: 10 }) .and_return(writer) expect(writer).to receive(:write) diff --git a/spec/table_structure/iterator_spec.rb b/spec/table_structure/iterator_spec.rb index 457d012..3b24fc2 100644 --- a/spec/table_structure/iterator_spec.rb +++ b/spec/table_structure/iterator_spec.rb @@ -9,19 +9,22 @@ let(:items) { users } context 'when :row_type is :array' do + include_context 'table_structured_array' + let(:iterator) do described_class.new( ::Mono::TestTableSchema.new(context: context), - **header_option, + **header_options, row_type: :array ) end context 'when :header is set true' do - let(:header_option) do + let(:header_options) do [ { header: true }, { header: { context: {} } }, + { header: { step: nil } }, {} ].sample end @@ -30,74 +33,59 @@ subject { iterator.iterate(items).map(&:itself) } it 'returns rows as array with header' do expect(subject.size).to eq 4 - - expect(subject[0]).to eq [ - 'ID', - 'Name', - 'Pet 1', - 'Pet 2', - 'Pet 3', - 'Q1', - 'Q2', - 'Q3' - ] - - expect(subject[1]).to eq [ - 1, - '太郎', - 'cat', - 'dog', - nil, - 'yes', - 'no', - 'yes' - ] - - expect(subject[2]).to eq [ - 2, - '花子', - 'rabbit', - 'turtle', - 'squirrel', - 'yes', - 'yes', - 'no' - ] - - expect(subject[3]).to eq [ - 3, - '次郎', - 'tiger', - 'elephant', - 'doragon', - 'no', - 'yes', - nil - ] + expect(subject[0]).to eq header_row + expect(subject[1]).to eq body_row_taro + expect(subject[2]).to eq body_row_hanako + expect(subject[3]).to eq body_row_jiro end end describe '#take' do - subject { iterator.iterate(items).take(1) } + subject { iterator.iterate(items).lazy.take(1).force } it 'returns rows as array with header' do expect(subject.size).to eq 1 + expect(subject[0]).to eq header_row + end + end + + context 'with positive step' do + let(:header_options) do + { header: { step: 2 } } + end - expect(subject[0]).to eq [ - 'ID', - 'Name', - 'Pet 1', - 'Pet 2', - 'Pet 3', - 'Q1', - 'Q2', - 'Q3' - ] + describe '#map' do + subject { iterator.iterate(items).map(&:itself) } + it 'returns rows as array with header' do + expect(subject.size).to eq 5 + expect(subject[0]).to eq header_row + expect(subject[1]).to eq body_row_taro + expect(subject[2]).to eq body_row_hanako + expect(subject[3]).to eq header_row + expect(subject[4]).to eq body_row_jiro + end + end + end + + context 'with negative step' do + let(:header_options) do + { header: { step: 0 } } + end + + describe '#map' do + subject { iterator.iterate(items).map(&:itself) } + it 'returns rows as array with header' do + expect(subject.size).to eq 4 + expect(subject[0]).to eq header_row + expect(subject[1]).to eq body_row_taro + expect(subject[2]).to eq body_row_hanako + expect(subject[3]).to eq body_row_jiro + end end end end context 'when :header is set false' do - let(:header_option) do + let(:header_options) do { header: false } end @@ -105,76 +93,39 @@ subject { iterator.iterate(items).map(&:itself) } it 'returns rows as array without header' do expect(subject.size).to eq 3 - - expect(subject[0]).to eq [ - 1, - '太郎', - 'cat', - 'dog', - nil, - 'yes', - 'no', - 'yes' - ] - - expect(subject[1]).to eq [ - 2, - '花子', - 'rabbit', - 'turtle', - 'squirrel', - 'yes', - 'yes', - 'no' - ] - - expect(subject[2]).to eq [ - 3, - '次郎', - 'tiger', - 'elephant', - 'doragon', - 'no', - 'yes', - nil - ] + expect(subject[0]).to eq body_row_taro + expect(subject[1]).to eq body_row_hanako + expect(subject[2]).to eq body_row_jiro end end describe '#take' do - subject { iterator.iterate(items).take(1) } + subject { iterator.iterate(items).lazy.take(1).force } it 'returns rows as array without header' do expect(subject.size).to eq 1 - - expect(subject[0]).to eq [ - 1, - '太郎', - 'cat', - 'dog', - nil, - 'yes', - 'no', - 'yes' - ] + expect(subject[0]).to eq body_row_taro end end end end context 'when :row_type is :hash' do + include_context 'table_structured_hash' + let(:iterator) do described_class.new( ::Mono::WithKeys::TestTableSchema.new(context: context), - **header_option, + **header_options, row_type: :hash ) end context 'when :header is set true' do - let(:header_option) do + let(:header_options) do [ { header: true }, { header: { context: {} } }, + { header: { step: nil } }, {} ].sample end @@ -183,74 +134,59 @@ subject { iterator.iterate(items).map(&:itself) } it 'returns rows as hash with header' do expect(subject.size).to eq 4 - - expect(subject[0]).to eq( - id: 'ID', - name: 'Name', - pet1: 'Pet 1', - pet2: 'Pet 2', - pet3: 'Pet 3', - q1: 'Q1', - q2: 'Q2', - q3: 'Q3' - ) - - expect(subject[1]).to eq( - id: 1, - name: '太郎', - pet1: 'cat', - pet2: 'dog', - pet3: nil, - q1: 'yes', - q2: 'no', - q3: 'yes' - ) - - expect(subject[2]).to eq( - id: 2, - name: '花子', - pet1: 'rabbit', - pet2: 'turtle', - pet3: 'squirrel', - q1: 'yes', - q2: 'yes', - q3: 'no' - ) - - expect(subject[3]).to eq( - id: 3, - name: '次郎', - pet1: 'tiger', - pet2: 'elephant', - pet3: 'doragon', - q1: 'no', - q2: 'yes', - q3: nil - ) + expect(subject[0]).to eq header_row + expect(subject[1]).to eq body_row_taro + expect(subject[2]).to eq body_row_hanako + expect(subject[3]).to eq body_row_jiro end end describe '#take' do - subject { iterator.iterate(items).take(1) } + subject { iterator.iterate(items).lazy.take(1).force } it 'returns rows as hash with header' do expect(subject.size).to eq 1 + expect(subject[0]).to eq header_row + end + end + + context 'with positive step' do + let(:header_options) do + { header: { step: 2 } } + end - expect(subject[0]).to eq( - id: 'ID', - name: 'Name', - pet1: 'Pet 1', - pet2: 'Pet 2', - pet3: 'Pet 3', - q1: 'Q1', - q2: 'Q2', - q3: 'Q3' - ) + describe '#map' do + subject { iterator.iterate(items).map(&:itself) } + it 'returns rows as hash with header' do + expect(subject.size).to eq 5 + expect(subject[0]).to eq header_row + expect(subject[1]).to eq body_row_taro + expect(subject[2]).to eq body_row_hanako + expect(subject[3]).to eq header_row + expect(subject[4]).to eq body_row_jiro + end + end + end + + context 'with negative step' do + let(:header_options) do + { header: { step: 0 } } + end + + describe '#map' do + subject { iterator.iterate(items).map(&:itself) } + it 'returns rows as hash with header' do + expect(subject.size).to eq 4 + expect(subject[0]).to eq header_row + expect(subject[1]).to eq body_row_taro + expect(subject[2]).to eq body_row_hanako + expect(subject[3]).to eq body_row_jiro + end end end end context 'when :header is set false' do - let(:header_option) do + let(:header_options) do { header: false } end @@ -258,57 +194,17 @@ subject { iterator.iterate(items).map(&:itself) } it 'returns rows as hash without header' do expect(subject.size).to eq 3 - - expect(subject[0]).to eq( - id: 1, - name: '太郎', - pet1: 'cat', - pet2: 'dog', - pet3: nil, - q1: 'yes', - q2: 'no', - q3: 'yes' - ) - - expect(subject[1]).to eq( - id: 2, - name: '花子', - pet1: 'rabbit', - pet2: 'turtle', - pet3: 'squirrel', - q1: 'yes', - q2: 'yes', - q3: 'no' - ) - - expect(subject[2]).to eq( - id: 3, - name: '次郎', - pet1: 'tiger', - pet2: 'elephant', - pet3: 'doragon', - q1: 'no', - q2: 'yes', - q3: nil - ) + expect(subject[0]).to eq body_row_taro + expect(subject[1]).to eq body_row_hanako + expect(subject[2]).to eq body_row_jiro end end describe '#take' do - subject { iterator.iterate(items).take(1) } + subject { iterator.iterate(items).lazy.take(1).force } it 'returns rows as hash without header' do expect(subject.size).to eq 1 - - expect(subject[0]).to eq( - id: 1, - name: '太郎', - pet1: 'cat', - pet2: 'dog', - pet3: nil, - q1: 'yes', - q2: 'no', - q3: 'yes' - ) + expect(subject[0]).to eq body_row_taro end end end diff --git a/spec/table_structure/table/iterator_spec.rb b/spec/table_structure/table/iterator_spec.rb deleted file mode 100644 index 7b906fd..0000000 --- a/spec/table_structure/table/iterator_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe TableStructure::Table::Iterator do - describe '#iterate' do - let(:iterator) { described_class.new(table, **options) } - let(:table) { double('TableStructure::Schema::Table') } - let(:enum) { iterator.iterate(items) } - let(:items) { ['body'] } - - context 'when header is enabled without context' do - let(:options) { { header: [true, {}].sample } } - - it 'iterates converted rows' do - expect(table).to receive(:header).with(context: nil).and_return('header') - expect(table).to receive(:body).with(items).and_return(items) - expect(enum.next).to eq 'header' - expect(enum.next).to eq 'body' - expect { enum.next }.to raise_error(::StopIteration) - end - end - - context 'when header is enabled with context' do - let(:options) { { header: { context: 'context' } } } - - it 'iterates converted rows' do - expect(table).to receive(:header).with(context: 'context').and_return('header') - expect(table).to receive(:body).with(items).and_return(items) - expect(enum.next).to eq 'header' - expect(enum.next).to eq 'body' - expect { enum.next }.to raise_error(::StopIteration) - end - end - end -end diff --git a/spec/table_structure/writer_spec.rb b/spec/table_structure/writer_spec.rb index b390a38..0346b9f 100644 --- a/spec/table_structure/writer_spec.rb +++ b/spec/table_structure/writer_spec.rb @@ -14,6 +14,8 @@ end context 'when output to CSV file' do + include_context 'table_structured_array_with_stringified' + shared_examples 'to convert and write data' do it 'succeeds' do require 'csv' @@ -33,49 +35,10 @@ table = ::CSV.read(tf.path, **csv_options) - expect(table[0]).to eq [ - 'ID', - 'Name', - 'Pet 1', - 'Pet 2', - 'Pet 3', - 'Q1', - 'Q2', - 'Q3' - ] - - expect(table[1]).to eq [ - '1', - '太郎', - 'cat', - 'dog', - '', - 'yes', - 'no', - 'yes' - ] - - expect(table[2]).to eq %w[ - 2 - 花子 - rabbit - turtle - squirrel - yes - yes - no - ] - - expect(table[3]).to eq [ - '3', - '次郎', - 'tiger', - 'elephant', - 'doragon', - 'no', - 'yes', - '' - ] + expect(table[0]).to eq header_row + expect(table[1]).to eq body_row_taro + expect(table[2]).to eq body_row_hanako + expect(table[3]).to eq body_row_jiro end end @@ -115,6 +78,8 @@ end context 'when output to yielder' do + include_context 'table_structured_array' + shared_examples 'to convert and write data' do it 'succeeds' do schema = ::Mono::TestTableSchema.new(context: context) @@ -127,52 +92,16 @@ end end - expect(enum.next).to eq [ - 'ID', - 'Name', - 'Pet 1', - 'Pet 2', - 'Pet 3', - 'Q1', - 'Q2', - 'Q3' - ] + expect(enum.next).to eq header_row expect(times).to eq 1 - expect(enum.next).to eq [ - 1, - '太郎', - 'cat', - 'dog', - nil, - 'yes', - 'no', - 'yes' - ] + expect(enum.next).to eq body_row_taro expect(times).to eq 2 - expect(enum.next).to eq [ - 2, - '花子', - 'rabbit', - 'turtle', - 'squirrel', - 'yes', - 'yes', - 'no' - ] + expect(enum.next).to eq body_row_hanako expect(times).to eq 3 - expect(enum.next).to eq [ - 3, - '次郎', - 'tiger', - 'elephant', - 'doragon', - 'no', - 'yes', - nil - ] + expect(enum.next).to eq body_row_jiro expect(times).to eq 4 end end @@ -190,6 +119,8 @@ context 'when output to array' do context 'with row_type: :array' do + include_context 'table_structured_array' + let(:options) do { row_type: :array } end @@ -199,49 +130,10 @@ table = [] writer.write(items, to: table) - expect(table[0]).to eq [ - 'ID', - 'Name', - 'Pet 1', - 'Pet 2', - 'Pet 3', - 'Q1', - 'Q2', - 'Q3' - ] - - expect(table[1]).to eq [ - 1, - '太郎', - 'cat', - 'dog', - nil, - 'yes', - 'no', - 'yes' - ] - - expect(table[2]).to eq [ - 2, - '花子', - 'rabbit', - 'turtle', - 'squirrel', - 'yes', - 'yes', - 'no' - ] - - expect(table[3]).to eq [ - 3, - '次郎', - 'tiger', - 'elephant', - 'doragon', - 'no', - 'yes', - nil - ] + expect(table[0]).to eq header_row + expect(table[1]).to eq body_row_taro + expect(table[2]).to eq body_row_hanako + expect(table[3]).to eq body_row_jiro end end @@ -260,6 +152,8 @@ end context 'with row_type: :hash' do + include_context 'table_structured_hash_with_index_keys' + let(:options) do { row_type: :hash } end @@ -269,49 +163,10 @@ table = [] writer.write(items, to: table) - expect(table[0]).to eq( - 0 => 'ID', - 1 => 'Name', - 2 => 'Pet 1', - 3 => 'Pet 2', - 4 => 'Pet 3', - 5 => 'Q1', - 6 => 'Q2', - 7 => 'Q3' - ) - - expect(table[1]).to eq( - 0 => 1, - 1 => '太郎', - 2 => 'cat', - 3 => 'dog', - 4 => nil, - 5 => 'yes', - 6 => 'no', - 7 => 'yes' - ) - - expect(table[2]).to eq( - 0 => 2, - 1 => '花子', - 2 => 'rabbit', - 3 => 'turtle', - 4 => 'squirrel', - 5 => 'yes', - 6 => 'yes', - 7 => 'no' - ) - - expect(table[3]).to eq( - 0 => 3, - 1 => '次郎', - 2 => 'tiger', - 3 => 'elephant', - 4 => 'doragon', - 5 => 'no', - 6 => 'yes', - 7 => nil - ) + expect(table[0]).to eq header_row + expect(table[1]).to eq body_row_taro + expect(table[2]).to eq body_row_hanako + expect(table[3]).to eq body_row_jiro end end