csv_plus_plus 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/csv_plus_plus/cell.rb +51 -0
- data/lib/csv_plus_plus/code_section.rb +49 -0
- data/lib/csv_plus_plus/color.rb +22 -0
- data/lib/csv_plus_plus/expand.rb +18 -0
- data/lib/csv_plus_plus/google_options.rb +23 -0
- data/lib/csv_plus_plus/graph.rb +68 -0
- data/lib/csv_plus_plus/language/cell_value.tab.rb +333 -0
- data/lib/csv_plus_plus/language/code_section.tab.rb +443 -0
- data/lib/csv_plus_plus/language/compiler.rb +170 -0
- data/lib/csv_plus_plus/language/entities/boolean.rb +32 -0
- data/lib/csv_plus_plus/language/entities/cell_reference.rb +26 -0
- data/lib/csv_plus_plus/language/entities/entity.rb +70 -0
- data/lib/csv_plus_plus/language/entities/function.rb +33 -0
- data/lib/csv_plus_plus/language/entities/function_call.rb +25 -0
- data/lib/csv_plus_plus/language/entities/number.rb +34 -0
- data/lib/csv_plus_plus/language/entities/runtime_value.rb +27 -0
- data/lib/csv_plus_plus/language/entities/string.rb +29 -0
- data/lib/csv_plus_plus/language/entities/variable.rb +25 -0
- data/lib/csv_plus_plus/language/entities.rb +28 -0
- data/lib/csv_plus_plus/language/references.rb +53 -0
- data/lib/csv_plus_plus/language/runtime.rb +147 -0
- data/lib/csv_plus_plus/language/scope.rb +199 -0
- data/lib/csv_plus_plus/language/syntax_error.rb +61 -0
- data/lib/csv_plus_plus/lexer/lexer.rb +64 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +65 -0
- data/lib/csv_plus_plus/lexer.rb +14 -0
- data/lib/csv_plus_plus/modifier.rb +124 -0
- data/lib/csv_plus_plus/modifier.tab.rb +921 -0
- data/lib/csv_plus_plus/options.rb +70 -0
- data/lib/csv_plus_plus/row.rb +42 -0
- data/lib/csv_plus_plus/template.rb +61 -0
- data/lib/csv_plus_plus/version.rb +6 -0
- data/lib/csv_plus_plus/writer/base_writer.rb +21 -0
- data/lib/csv_plus_plus/writer/csv.rb +31 -0
- data/lib/csv_plus_plus/writer/excel.rb +13 -0
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +173 -0
- data/lib/csv_plus_plus/writer/google_sheets.rb +139 -0
- data/lib/csv_plus_plus/writer/open_document.rb +14 -0
- data/lib/csv_plus_plus/writer.rb +25 -0
- data/lib/csv_plus_plus.rb +20 -0
- metadata +83 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './google_options'
|
4
|
+
|
5
|
+
module CSVPlusPlus
|
6
|
+
# The options a user can supply
|
7
|
+
class Options
|
8
|
+
attr_accessor :backup, :create_if_not_exists, :key_values, :offset, :output_filename, :sheet_name, :verbose
|
9
|
+
attr_reader :google
|
10
|
+
|
11
|
+
# initialize
|
12
|
+
def initialize
|
13
|
+
@offset = [0, 0]
|
14
|
+
@create_if_not_exists = false
|
15
|
+
@key_values = {}
|
16
|
+
@verbose = false
|
17
|
+
# TODO: switch to true? probably a safer choice
|
18
|
+
@backup = false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the Google Sheet ID
|
22
|
+
def google_sheet_id=(sheet_id)
|
23
|
+
@google = ::CSVPlusPlus::GoogleOptions.new(sheet_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns an error string or nil if there are no validation problems
|
27
|
+
def validate
|
28
|
+
return if @google || @output_filename
|
29
|
+
|
30
|
+
'You must supply either a Google Sheet ID or an output file'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return a string with a verbose description of what we're doing with the options
|
34
|
+
def verbose_summary
|
35
|
+
<<~SUMMARY
|
36
|
+
#{summary_divider}
|
37
|
+
|
38
|
+
# csv++ Command Options
|
39
|
+
|
40
|
+
> Input filename | #{@filename}
|
41
|
+
> Sheet name | #{@sheet_name}
|
42
|
+
> Create sheet if it does not exist? | #{@create_if_not_exists}
|
43
|
+
> Spreadsheet row-offset | #{@offset[0]}
|
44
|
+
> Spreadsheet cell-offset | #{@offset[1]}
|
45
|
+
> User-supplied key-values | #{@key_values}
|
46
|
+
> Verbose | #{@verbose}
|
47
|
+
|
48
|
+
## Output Options
|
49
|
+
|
50
|
+
> Backup | #{@backup}
|
51
|
+
> Output filename | #{@output_filename}
|
52
|
+
|
53
|
+
#{@google&.verbose_summary || ''}
|
54
|
+
#{summary_divider}
|
55
|
+
SUMMARY
|
56
|
+
end
|
57
|
+
|
58
|
+
# to_s
|
59
|
+
def to_s
|
60
|
+
"Options(create_if_not_exists: #{@create_if_not_exists}, google: #{@google}, key_values: #{@key_values}, " \
|
61
|
+
"offset: #{@offset}, sheet_name: #{@sheet_name}, verbose: #{@verbose})"
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def summary_divider
|
67
|
+
'========================================================================='
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'cell'
|
4
|
+
require_relative 'modifier.tab'
|
5
|
+
|
6
|
+
module CSVPlusPlus
|
7
|
+
##
|
8
|
+
# A row of a template
|
9
|
+
class Row
|
10
|
+
attr_reader :cells, :index, :modifier
|
11
|
+
|
12
|
+
# initialize
|
13
|
+
def initialize(index, cells, modifier)
|
14
|
+
@cells = cells
|
15
|
+
@modifier = modifier
|
16
|
+
@index = index
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set the row index. And update the index of all affected cells
|
20
|
+
def index=(index)
|
21
|
+
@index = index
|
22
|
+
@cells.each { |cell| cell.row_index = index }
|
23
|
+
end
|
24
|
+
|
25
|
+
# How much this row will expand itself, if at all (0)
|
26
|
+
def expand_amount
|
27
|
+
return 0 unless @modifier.expand
|
28
|
+
|
29
|
+
@modifier.expand.repetitions || (1000 - @index)
|
30
|
+
end
|
31
|
+
|
32
|
+
# to_s
|
33
|
+
def to_s
|
34
|
+
"Row(index: #{index}, modifier: #{modifier}, cells: #{cells})"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return a deep copy of this row
|
38
|
+
def deep_clone
|
39
|
+
::Marshal.load(::Marshal.dump(self))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
# Contains the flow and data from a code section and CSV section
|
5
|
+
class Template
|
6
|
+
attr_reader :rows, :scope
|
7
|
+
|
8
|
+
# initialize
|
9
|
+
def initialize(rows:, scope:)
|
10
|
+
@rows = rows
|
11
|
+
@scope = scope
|
12
|
+
end
|
13
|
+
|
14
|
+
# to_s
|
15
|
+
def to_s
|
16
|
+
"Template(rows: #{@rows}, scope: #{@scope})"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Apply any expand= modifiers to the parsed template
|
20
|
+
def expand_rows!
|
21
|
+
expanded_rows = []
|
22
|
+
row_index = 0
|
23
|
+
expand_rows(
|
24
|
+
lambda do |new_row|
|
25
|
+
new_row.index = row_index
|
26
|
+
expanded_rows << new_row
|
27
|
+
row_index += 1
|
28
|
+
end
|
29
|
+
)
|
30
|
+
|
31
|
+
@rows = expanded_rows
|
32
|
+
end
|
33
|
+
|
34
|
+
# Make sure that the template has a valid amount of infinite expand modifiers
|
35
|
+
def validate_infinite_expands(runtime)
|
36
|
+
infinite_expand_rows = @rows.filter { |r| r.modifier.expand&.infinite? }
|
37
|
+
return unless infinite_expand_rows.length > 1
|
38
|
+
|
39
|
+
runtime.raise_syntax_error(
|
40
|
+
'You can only have one infinite expand= (on all others you must specify an amount)',
|
41
|
+
infinite_expand_rows[1]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def expand_rows(push_row_fn)
|
48
|
+
# TODO: make it so that an infinite expand will not overwrite the rows below it, but
|
49
|
+
# instead merge with them
|
50
|
+
rows.each do |row|
|
51
|
+
if row.modifier.expand
|
52
|
+
row.expand_amount.times do
|
53
|
+
push_row_fn.call(row.deep_clone)
|
54
|
+
end
|
55
|
+
else
|
56
|
+
push_row_fn.call(row)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Writer
|
5
|
+
##
|
6
|
+
# Some shared functionality that all Writers should build on
|
7
|
+
class BaseWriter
|
8
|
+
attr_accessor :options
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Open a CSV outputter to +filename+
|
13
|
+
def initialize(options)
|
14
|
+
@options = options
|
15
|
+
load_requires
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_requires; end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Writer
|
5
|
+
##
|
6
|
+
# A class that can output a +Template+ to CSV
|
7
|
+
class CSV < ::CSVPlusPlus::Writer::BaseWriter
|
8
|
+
# write a +template+ to CSV
|
9
|
+
def write(template)
|
10
|
+
# TODO: also read it and merge the results
|
11
|
+
::CSV.open(@options.output_filename, 'wb') do |csv|
|
12
|
+
template.rows.each do |row|
|
13
|
+
csv << build_row(row)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def load_requires
|
21
|
+
require('csv')
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_row(row)
|
27
|
+
row.cells.map(&:to_csv)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Writer
|
5
|
+
# A class that can output a +Template+ to an Excel file
|
6
|
+
class Excel < ::CSVPlusPlus::Writer::BaseWriter
|
7
|
+
# write a +template+ to an Excel file
|
8
|
+
def write(template)
|
9
|
+
# TODO
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Writer
|
5
|
+
##
|
6
|
+
# Given +rows+ from a +Template+, build requests compatible with Google Sheets Ruby API
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
8
|
+
class GoogleSheetBuilder
|
9
|
+
# initialize
|
10
|
+
def initialize(current_sheet_values:, sheet_id:, rows:, column_index: 0, row_index: 0)
|
11
|
+
@current_sheet_values = current_sheet_values
|
12
|
+
@sheet_id = sheet_id
|
13
|
+
@rows = rows
|
14
|
+
@column_index = column_index
|
15
|
+
@row_index = row_index
|
16
|
+
end
|
17
|
+
|
18
|
+
# Build a Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest
|
19
|
+
def batch_update_spreadsheet_request
|
20
|
+
build_batch_request(@rows)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def sheets_ns
|
26
|
+
::Google::Apis::SheetsV4
|
27
|
+
end
|
28
|
+
|
29
|
+
def sheets_color(color)
|
30
|
+
sheets_ns::Color.new(red: color.red, green: color.green, blue: color.blue)
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_extended_value_type!(extended_value, value)
|
34
|
+
v = value || ''
|
35
|
+
if v.start_with?('=')
|
36
|
+
extended_value.formula_value = value
|
37
|
+
elsif v.match(/^-?[\d.]+$/)
|
38
|
+
extended_value.number_value = value
|
39
|
+
elsif v.downcase == 'true' || v.downcase == 'false'
|
40
|
+
extended_value.boolean_value = value
|
41
|
+
else
|
42
|
+
extended_value.string_value = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_text_format(mod)
|
47
|
+
sheets_ns::TextFormat.new(
|
48
|
+
bold: mod.formatted?('bold') || nil,
|
49
|
+
italic: mod.formatted?('italic') || nil,
|
50
|
+
strikethrough: mod.formatted?('strikethrough') || nil,
|
51
|
+
underline: mod.formatted?('underline') || nil,
|
52
|
+
|
53
|
+
font_family: mod.fontfamily,
|
54
|
+
font_size: mod.fontsize,
|
55
|
+
|
56
|
+
foreground_color: mod.fontcolor ? sheets_color(mod.fontcolor) : nil
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
61
|
+
def build_cell_format(mod)
|
62
|
+
sheets_ns::CellFormat.new.tap do |cf|
|
63
|
+
cf.text_format = build_text_format(mod)
|
64
|
+
|
65
|
+
# TODO: are these not overwriting each other?
|
66
|
+
cf.horizontal_alignment = 'LEFT' if mod.aligned?('left')
|
67
|
+
cf.horizontal_alignment = 'RIGHT' if mod.aligned?('right')
|
68
|
+
cf.horizontal_alignment = 'CENTER' if mod.aligned?('center')
|
69
|
+
cf.vertical_alignment = 'TOP' if mod.aligned?('top')
|
70
|
+
cf.vertical_alignment = 'BOTTOM' if mod.aligned?('bottom')
|
71
|
+
|
72
|
+
cf.background_color = sheets_color(mod.color) if mod.color
|
73
|
+
|
74
|
+
cf.number_format = sheets_ns::NumberFormat.new(type: mod.numberformat) if mod.numberformat
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
78
|
+
|
79
|
+
def grid_range_for_cell(cell)
|
80
|
+
sheets_ns::GridRange.new(
|
81
|
+
sheet_id: @sheet_id,
|
82
|
+
start_column_index: cell.index,
|
83
|
+
end_column_index: cell.index + 1,
|
84
|
+
start_row_index: cell.row_index,
|
85
|
+
end_row_index: cell.row_index + 1
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def current_value(row_index, cell_index)
|
90
|
+
@current_values[row_index][cell_index]
|
91
|
+
rescue ::StandardError
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def build_cell_value(cell)
|
96
|
+
sheets_ns::ExtendedValue.new.tap do |xv|
|
97
|
+
value =
|
98
|
+
if cell.value.nil?
|
99
|
+
current_value(cell.row_index, cell.index)
|
100
|
+
else
|
101
|
+
cell.to_csv
|
102
|
+
end
|
103
|
+
|
104
|
+
set_extended_value_type!(xv, value)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_cell_data(cell)
|
109
|
+
mod = cell.modifier
|
110
|
+
|
111
|
+
sheets_ns::CellData.new.tap do |cd|
|
112
|
+
cd.user_entered_format = build_cell_format(cell.modifier)
|
113
|
+
cd.note = mod.note if mod.note
|
114
|
+
|
115
|
+
# XXX apply data validation
|
116
|
+
cd.user_entered_value = build_cell_value(cell)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def build_row_data(row)
|
121
|
+
sheets_ns::RowData.new(values: row.cells.map { |cell| build_cell_data(cell) })
|
122
|
+
end
|
123
|
+
|
124
|
+
def build_update_cells_request(rows)
|
125
|
+
sheets_ns::UpdateCellsRequest.new(
|
126
|
+
fields: '*',
|
127
|
+
start: sheets_ns::GridCoordinate.new(
|
128
|
+
sheet_id: @sheet_id,
|
129
|
+
column_index: @column_index,
|
130
|
+
row_index: @row_index
|
131
|
+
),
|
132
|
+
rows: rows.map { |row| build_row_data(row) }
|
133
|
+
)
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_border(cell)
|
137
|
+
mod = cell.modifier
|
138
|
+
# TODO: allow different border styles per side
|
139
|
+
border = sheets_ns::Border.new(color: mod.bordercolor || '#000000', style: mod.borderstyle || 'solid')
|
140
|
+
sheets_ns::UpdateBordersRequest.new(
|
141
|
+
top: mod.border_along?('top') ? border : nil,
|
142
|
+
right: mod.border_along?('right') ? border : nil,
|
143
|
+
left: mod.border_along?('left') ? border : nil,
|
144
|
+
bottom: mod.border_along?('bottom') ? border : nil,
|
145
|
+
range: grid_range_for_cell(cell)
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
def build_update_borders_request(cell)
|
150
|
+
sheets_ns::Request.new(update_borders: build_border(cell))
|
151
|
+
end
|
152
|
+
|
153
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
154
|
+
def build_batch_request(rows)
|
155
|
+
sheets_ns::BatchUpdateSpreadsheetRequest.new.tap do |bu|
|
156
|
+
bu.requests =
|
157
|
+
rows.each_slice(1000).to_a.map do |chunked_rows|
|
158
|
+
sheets_ns::Request.new(update_cells: build_update_cells_request(chunked_rows))
|
159
|
+
end
|
160
|
+
|
161
|
+
rows.each do |row|
|
162
|
+
row.cells.filter { |c| c.modifier.any_border? }
|
163
|
+
.each do |cell|
|
164
|
+
bu.requests << build_update_borders_request(cell)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
170
|
+
end
|
171
|
+
# rubocop:enable Metrics/ClassLength
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base_writer'
|
4
|
+
require_relative 'google_sheet_builder'
|
5
|
+
|
6
|
+
AUTH_SCOPES = ['https://www.googleapis.com/auth/spreadsheets'].freeze
|
7
|
+
FULL_RANGE = 'A1:Z1000'
|
8
|
+
|
9
|
+
module CSVPlusPlus
|
10
|
+
module Writer
|
11
|
+
# A class that can output a +Template+ to Google Sheets (via their API)
|
12
|
+
class GoogleSheets < ::CSVPlusPlus::Writer::BaseWriter
|
13
|
+
# XXX it would be nice to raise this but we shouldn't expand out more than necessary for our data
|
14
|
+
SPREADSHEET_INFINITY = 1000
|
15
|
+
public_constant :SPREADSHEET_INFINITY
|
16
|
+
|
17
|
+
# initialize
|
18
|
+
def initialize(options)
|
19
|
+
super(options)
|
20
|
+
|
21
|
+
@sheet_id = options.google.sheet_id
|
22
|
+
@sheet_name = options.sheet_name
|
23
|
+
end
|
24
|
+
|
25
|
+
# write a +template+ to Google Sheets
|
26
|
+
def write(template)
|
27
|
+
auth!
|
28
|
+
|
29
|
+
save_spreadsheet!
|
30
|
+
save_spreadsheet_values!
|
31
|
+
|
32
|
+
create_sheet! if @options.create_if_not_exists
|
33
|
+
|
34
|
+
update_cells!(template)
|
35
|
+
rescue ::Google::Apis::ClientError => e
|
36
|
+
handle_google_error(e)
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def load_requires
|
42
|
+
require('google/apis/sheets_v4')
|
43
|
+
require('googleauth')
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def format_range(range)
|
49
|
+
@sheet_name ? "'#{@sheet_name}'!#{range}" : range
|
50
|
+
end
|
51
|
+
|
52
|
+
def full_range
|
53
|
+
format_range(::FULL_RANGE)
|
54
|
+
end
|
55
|
+
|
56
|
+
def auth!
|
57
|
+
@gs ||= sheets_ns::SheetsService.new
|
58
|
+
@gs.authorization = ::Google::Auth.get_application_default(::AUTH_SCOPES)
|
59
|
+
end
|
60
|
+
|
61
|
+
def save_spreadsheet_values!
|
62
|
+
formatted_values = get_all_spreadsheet_values('FORMATTED_VALUE')
|
63
|
+
formula_values = get_all_spreadsheet_values('FORMULA')
|
64
|
+
|
65
|
+
return if formula_values.values.nil? || formatted_values.values.nil?
|
66
|
+
|
67
|
+
@current_values = extract_current_values(formatted_values, formula_values)
|
68
|
+
end
|
69
|
+
|
70
|
+
def extract_current_values(formatted_values, formula_values)
|
71
|
+
formatted_values.values.map.each_with_index do |row, x|
|
72
|
+
row.map.each_with_index do |_cell, y|
|
73
|
+
formula_value = formula_values.values[x][y]
|
74
|
+
if formula_value.is_a?(::String) && formula_value.start_with?('=')
|
75
|
+
formula_value
|
76
|
+
else
|
77
|
+
strip_to_nil(formatted_values.values[x][y])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def strip_to_nil(str)
|
84
|
+
str.strip.empty? ? nil : str
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_all_spreadsheet_values(render_option)
|
88
|
+
@gs.get_spreadsheet_values(@sheet_id, full_range, value_render_option: render_option)
|
89
|
+
end
|
90
|
+
|
91
|
+
def sheet
|
92
|
+
return unless @sheet_name
|
93
|
+
|
94
|
+
@spreadsheet.sheets.find { |s| s.properties.title.strip == @sheet_name.strip }
|
95
|
+
end
|
96
|
+
|
97
|
+
def save_spreadsheet!
|
98
|
+
@spreadsheet = @gs.get_spreadsheet(@sheet_id)
|
99
|
+
|
100
|
+
return unless @sheet_name.nil?
|
101
|
+
|
102
|
+
@sheet_name = @spreadsheet.sheets&.first&.properties&.title
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_sheet!
|
106
|
+
return if sheet
|
107
|
+
|
108
|
+
@gs.create_spreadsheet(@sheet_name)
|
109
|
+
get_spreadsheet!
|
110
|
+
@sheet_name = @spreadsheet.sheets.last.properties.title
|
111
|
+
end
|
112
|
+
|
113
|
+
def update_cells!(template)
|
114
|
+
builder = ::CSVPlusPlus::Writer::GoogleSheetBuilder.new(
|
115
|
+
rows: template.rows,
|
116
|
+
sheet_id: sheet&.properties&.sheet_id,
|
117
|
+
column_index: @options.offset[1],
|
118
|
+
row_index: @options.offset[0],
|
119
|
+
current_sheet_values: @current_sheet_values
|
120
|
+
)
|
121
|
+
@gs.batch_update_spreadsheet(@sheet_id, builder.batch_update_spreadsheet_request)
|
122
|
+
rescue ::Google::Apis::ClientError => e
|
123
|
+
handle_google_error(e)
|
124
|
+
end
|
125
|
+
|
126
|
+
def sheets_ns
|
127
|
+
::Google::Apis::SheetsV4
|
128
|
+
end
|
129
|
+
|
130
|
+
def handle_google_error(error)
|
131
|
+
if @options.verbose
|
132
|
+
warn("#{error.status_code} Error making Google Sheets API request [#{error.message}]: #{error.body}")
|
133
|
+
else
|
134
|
+
warn("Error making Google Sheets API request: #{error.message}")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CSVPlusPlus
|
4
|
+
module Writer
|
5
|
+
##
|
6
|
+
# A class that can output a +Template+ to an Excel file
|
7
|
+
class OpenDocument < ::CSVPlusPlus::Writer::BaseWriter
|
8
|
+
# write a +template+ to an OpenDocument file
|
9
|
+
def write(template)
|
10
|
+
# TODO
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './writer/base_writer'
|
4
|
+
require_relative './writer/csv'
|
5
|
+
require_relative './writer/excel'
|
6
|
+
require_relative './writer/google_sheets'
|
7
|
+
require_relative './writer/open_document'
|
8
|
+
|
9
|
+
module CSVPlusPlus
|
10
|
+
##
|
11
|
+
# Various strategies for writing to various formats (excel, google sheets, CSV, OpenDocument)
|
12
|
+
module Writer
|
13
|
+
# Return an instance of a writer depending on the given +options+
|
14
|
+
def self.writer(options)
|
15
|
+
return ::CSVPlusPlus::Writer::GoogleSheets.new(options) if options.google
|
16
|
+
|
17
|
+
case options.output_filename
|
18
|
+
when /\.csv$/ then ::CSVPlusPlus::Writer::CSV.new(options)
|
19
|
+
when /\.ods$/ then ::CSVPlusPlus::Writer::OpenDocument.new(options)
|
20
|
+
when /\.xls$/ then ::CSVPlusPlus::Writer::Excel.new(options)
|
21
|
+
else raise(::StandardError, "Unsupported extension: #{options.output_filename}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'csv_plus_plus/language/compiler'
|
4
|
+
require_relative 'csv_plus_plus/options'
|
5
|
+
require_relative 'csv_plus_plus/writer'
|
6
|
+
|
7
|
+
# A language for writing rich CSV data
|
8
|
+
module CSVPlusPlus
|
9
|
+
# Parse the input into a +Template+ and write it to the desired format
|
10
|
+
def self.apply_template_to_sheet!(input, filename, options)
|
11
|
+
warn(options.verbose_summary) if options.verbose
|
12
|
+
|
13
|
+
::CSVPlusPlus::Language::Compiler.with_compiler(input:, filename:, options:) do |c|
|
14
|
+
template = c.parse_template
|
15
|
+
|
16
|
+
output = ::CSVPlusPlus::Writer.writer(options)
|
17
|
+
c.outputting! { output.write(template) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|