csv_plus_plus 0.1.2 → 0.2.0
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 +4 -4
- data/README.md +9 -5
- data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
- data/lib/csv_plus_plus/a1_reference.rb +202 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
- data/lib/csv_plus_plus/cell.rb +29 -41
- data/lib/csv_plus_plus/cli.rb +53 -80
- data/lib/csv_plus_plus/cli_flag.rb +71 -71
- data/lib/csv_plus_plus/color.rb +32 -7
- data/lib/csv_plus_plus/compiler.rb +98 -66
- data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
- data/lib/csv_plus_plus/entities/boolean.rb +26 -10
- data/lib/csv_plus_plus/entities/builtins.rb +66 -24
- data/lib/csv_plus_plus/entities/date.rb +42 -6
- data/lib/csv_plus_plus/entities/entity.rb +17 -69
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
- data/lib/csv_plus_plus/entities/function.rb +34 -11
- data/lib/csv_plus_plus/entities/function_call.rb +49 -10
- data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
- data/lib/csv_plus_plus/entities/number.rb +30 -11
- data/lib/csv_plus_plus/entities/reference.rb +77 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
- data/lib/csv_plus_plus/entities/string.rb +23 -7
- data/lib/csv_plus_plus/entities.rb +7 -16
- data/lib/csv_plus_plus/error/cli_error.rb +17 -0
- data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
- data/lib/csv_plus_plus/error/error.rb +25 -2
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
- data/lib/csv_plus_plus/error/positional_error.rb +15 -0
- data/lib/csv_plus_plus/error/writer_error.rb +8 -0
- data/lib/csv_plus_plus/error.rb +5 -1
- data/lib/csv_plus_plus/error_formatter.rb +111 -0
- data/lib/csv_plus_plus/google_api_client.rb +25 -10
- data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
- data/lib/csv_plus_plus/lexer.rb +64 -1
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
- data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
- data/lib/csv_plus_plus/modifier/expand.rb +78 -0
- data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
- data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
- data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
- data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
- data/lib/csv_plus_plus/modifier.rb +89 -160
- data/lib/csv_plus_plus/options/file_options.rb +49 -0
- data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
- data/lib/csv_plus_plus/options/options.rb +97 -0
- data/lib/csv_plus_plus/options.rb +34 -77
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
- data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
- data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
- data/lib/csv_plus_plus/reader/csv.rb +50 -0
- data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
- data/lib/csv_plus_plus/reader/reader.rb +27 -0
- data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
- data/lib/csv_plus_plus/reader.rb +14 -0
- data/lib/csv_plus_plus/row.rb +53 -12
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position.rb +242 -0
- data/lib/csv_plus_plus/runtime/references.rb +115 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
- data/lib/csv_plus_plus/runtime/scope.rb +280 -0
- data/lib/csv_plus_plus/runtime.rb +34 -191
- data/lib/csv_plus_plus/source_code.rb +71 -0
- data/lib/csv_plus_plus/template.rb +71 -39
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/csv.rb +37 -8
- data/lib/csv_plus_plus/writer/excel.rb +25 -5
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
- data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
- data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
- data/lib/csv_plus_plus/writer/merger.rb +31 -0
- data/lib/csv_plus_plus/writer/open_document.rb +21 -2
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
- data/lib/csv_plus_plus/writer/writer.rb +42 -0
- data/lib/csv_plus_plus/writer.rb +79 -10
- data/lib/csv_plus_plus.rb +47 -18
- metadata +50 -21
- data/lib/csv_plus_plus/can_define_references.rb +0 -88
- data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
- data/lib/csv_plus_plus/data_validation.rb +0 -138
- data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
- data/lib/csv_plus_plus/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
- data/lib/csv_plus_plus/expand.rb +0 -20
- data/lib/csv_plus_plus/google_options.rb +0 -27
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
- data/lib/csv_plus_plus/references.rb +0 -68
- data/lib/csv_plus_plus/scope.rb +0 -196
- data/lib/csv_plus_plus/validated_modifier.rb +0 -164
- data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
- data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
|
@@ -1,62 +1,96 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module CSVPlusPlus
|
|
4
5
|
# Contains the data from a parsed csvpp template.
|
|
5
6
|
#
|
|
6
7
|
# @attr_reader rows [Array<Row>] The +Row+s that comprise this +Template+
|
|
7
|
-
# @attr_reader
|
|
8
|
+
# @attr_reader runtime [Runtime] The +Runtime+ containing all function and variable references
|
|
8
9
|
class Template
|
|
9
|
-
|
|
10
|
+
extend ::T::Sig
|
|
10
11
|
|
|
12
|
+
sig { returns(::T::Array[::CSVPlusPlus::Row]) }
|
|
13
|
+
attr_reader :rows
|
|
14
|
+
|
|
15
|
+
sig { returns(::CSVPlusPlus::Runtime::Runtime) }
|
|
16
|
+
attr_reader :runtime
|
|
17
|
+
|
|
18
|
+
sig { params(rows: ::T::Array[::CSVPlusPlus::Row], runtime: ::CSVPlusPlus::Runtime::Runtime).void }
|
|
11
19
|
# @param rows [Array<Row>] The +Row+s that comprise this +Template+
|
|
12
|
-
# @param
|
|
13
|
-
def initialize(rows:,
|
|
14
|
-
@scope = scope
|
|
20
|
+
# @param runtime [Runtime] The +Runtime+ containing all function and variable references
|
|
21
|
+
def initialize(rows:, runtime:)
|
|
15
22
|
@rows = rows
|
|
23
|
+
@runtime = runtime
|
|
16
24
|
end
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).void }
|
|
27
|
+
# Only run after expanding all rows, now we can bind all [[var=]] modifiers to a variable. There are two distinct
|
|
28
|
+
# types of variable bindings here:
|
|
29
|
+
#
|
|
30
|
+
# * Binding to a cell: for this we just make an +A1Reference+ to the cell itself (A1, B4, etc)
|
|
31
|
+
# * Binding to a cell within an expand: the variable can only be resolved within that expand and needs to be
|
|
32
|
+
# relative to it's row (it can't be an absolute cell reference like above)
|
|
33
|
+
#
|
|
34
|
+
# @param runtime [Runtime] The current runtime
|
|
35
|
+
# rubocop:disable Metrics/MethodLength
|
|
36
|
+
def bind_all_vars!(runtime)
|
|
37
|
+
runtime.position.map_rows(@rows) do |row|
|
|
38
|
+
# rubocop:disable Style/MissingElse
|
|
39
|
+
if row.unexpanded?
|
|
40
|
+
# rubocop:enable Style/MissingElse
|
|
41
|
+
raise(
|
|
42
|
+
::CSVPlusPlus::Error::CompilerError,
|
|
43
|
+
'Template#expand_rows! must be called before Template#bind_all_vars!'
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
runtime.position.map_row(row.cells) do |cell|
|
|
48
|
+
bind_vars(cell, row.modifier.expand)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
21
51
|
end
|
|
52
|
+
# rubocop:enable Metrics/MethodLength
|
|
22
53
|
|
|
23
|
-
|
|
54
|
+
sig { returns(::T::Array[::CSVPlusPlus::Row]) }
|
|
55
|
+
# Apply expand= (adding rows to the results) modifiers to the parsed template. This happens in towards the end of
|
|
56
|
+
# compilation because expanding rows will change the relative rownums as rows are added, and variables can't be
|
|
57
|
+
# bound until the rows have been assigned their final rownums.
|
|
24
58
|
#
|
|
25
59
|
# @return [Array<Row>]
|
|
26
60
|
def expand_rows!
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
61
|
+
# TODO: make it so that an infinite expand will not overwrite the rows below it, but instead merge with them
|
|
62
|
+
@rows =
|
|
63
|
+
rows.reduce([]) do |expanded_rows, row|
|
|
64
|
+
if row.modifier.expand
|
|
65
|
+
row.expand_rows(starts_at: expanded_rows.length, into: expanded_rows)
|
|
66
|
+
else
|
|
67
|
+
expanded_rows << row.tap { |r| r.index = expanded_rows.length }
|
|
68
|
+
end
|
|
34
69
|
end
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
@rows = expanded_rows
|
|
38
70
|
end
|
|
39
71
|
|
|
72
|
+
sig { void }
|
|
40
73
|
# Make sure that the template has a valid amount of infinite expand modifiers
|
|
41
|
-
|
|
42
|
-
# @param runtime [Runtime] The compiler's current runtime
|
|
43
|
-
def validate_infinite_expands(runtime)
|
|
74
|
+
def validate_infinite_expands
|
|
44
75
|
infinite_expand_rows = @rows.filter { |r| r.modifier.expand&.infinite? }
|
|
45
76
|
return unless infinite_expand_rows.length > 1
|
|
46
77
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
raise(
|
|
79
|
+
::CSVPlusPlus::Error::ModifierSyntaxError.new(
|
|
80
|
+
'You can only have one infinite expand= (on all others you must specify an amount)',
|
|
81
|
+
bad_input: infinite_expand_rows[1].to_s
|
|
82
|
+
)
|
|
50
83
|
)
|
|
51
84
|
end
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
sig { returns(::String) }
|
|
87
|
+
# Provide a summary of the state of the template (and it's +@runtime+)
|
|
54
88
|
#
|
|
55
|
-
# @return [String]
|
|
89
|
+
# @return [::String]
|
|
56
90
|
def verbose_summary
|
|
57
91
|
# TODO: we can probably include way more stats in here
|
|
58
92
|
<<~SUMMARY
|
|
59
|
-
#{@scope.verbose_summary}
|
|
93
|
+
#{@runtime.scope.verbose_summary}
|
|
60
94
|
|
|
61
95
|
> #{@rows.length} rows to be written
|
|
62
96
|
SUMMARY
|
|
@@ -64,17 +98,15 @@ module CSVPlusPlus
|
|
|
64
98
|
|
|
65
99
|
private
|
|
66
100
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
push_row_fn.call(row)
|
|
77
|
-
end
|
|
101
|
+
sig { params(cell: ::CSVPlusPlus::Cell, expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)).void }
|
|
102
|
+
def bind_vars(cell, expand)
|
|
103
|
+
var = cell.modifier.var
|
|
104
|
+
return unless var
|
|
105
|
+
|
|
106
|
+
if expand
|
|
107
|
+
@runtime.bind_variable_in_expand(var, expand)
|
|
108
|
+
else
|
|
109
|
+
@runtime.bind_variable_to_cell(var)
|
|
78
110
|
end
|
|
79
111
|
end
|
|
80
112
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative './file_backer_upper'
|
|
@@ -5,29 +6,57 @@ require_relative './file_backer_upper'
|
|
|
5
6
|
module CSVPlusPlus
|
|
6
7
|
module Writer
|
|
7
8
|
# A class that can output a +Template+ to CSV
|
|
8
|
-
class CSV < ::CSVPlusPlus::Writer::
|
|
9
|
+
class CSV < ::CSVPlusPlus::Writer::Writer
|
|
10
|
+
extend ::T::Sig
|
|
9
11
|
include ::CSVPlusPlus::Writer::FileBackerUpper
|
|
12
|
+
include ::CSVPlusPlus::Writer::Merger
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
sig { params(options: ::CSVPlusPlus::Options::FileOptions, position: ::CSVPlusPlus::Runtime::Position).void }
|
|
15
|
+
# @param options [Options::FileOptions]
|
|
16
|
+
# @param position [Runtime::Position]
|
|
17
|
+
def initialize(options, position)
|
|
18
|
+
super(position)
|
|
19
|
+
|
|
20
|
+
@reader = ::T.let(::CSVPlusPlus::Reader::CSV.new(options), ::CSVPlusPlus::Reader::CSV)
|
|
21
|
+
@options = options
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { override.params(template: ::CSVPlusPlus::Template).void }
|
|
25
|
+
# Write a +template+ to CSV
|
|
26
|
+
#
|
|
27
|
+
# @param template [Template] The template to use as input to be written. It should have been compiled by calling
|
|
28
|
+
# Compiler#compile_template
|
|
12
29
|
def write(template)
|
|
13
|
-
# TODO: also read it and merge the results
|
|
14
30
|
::CSV.open(@options.output_filename, 'wb') do |csv|
|
|
15
|
-
template.rows
|
|
31
|
+
@position.map_rows(template.rows) do |row|
|
|
16
32
|
csv << build_row(row)
|
|
17
33
|
end
|
|
18
34
|
end
|
|
19
35
|
end
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
sig { override.void }
|
|
38
|
+
# Write a backup of the current spreadsheet.
|
|
39
|
+
def write_backup
|
|
40
|
+
backup_file(@options)
|
|
41
|
+
end
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
sig { params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::String)) }
|
|
44
|
+
# Turn the cell into a CSV-
|
|
45
|
+
def evaluate_cell(cell)
|
|
46
|
+
if (ast = cell.ast)
|
|
47
|
+
"=#{ast.evaluate(@position)}"
|
|
48
|
+
else
|
|
49
|
+
cell.value
|
|
50
|
+
end
|
|
25
51
|
end
|
|
26
52
|
|
|
27
53
|
private
|
|
28
54
|
|
|
55
|
+
sig { params(row: ::CSVPlusPlus::Row).returns(::T::Array[::T.nilable(::String)]) }
|
|
29
56
|
def build_row(row)
|
|
30
|
-
row.cells
|
|
57
|
+
@position.map_row(row.cells) do |cell, _i|
|
|
58
|
+
merge_cell_value(existing_value: @reader.value_at(cell), new_value: evaluate_cell(cell), options: @options)
|
|
59
|
+
end
|
|
31
60
|
end
|
|
32
61
|
end
|
|
33
62
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require_relative './file_backer_upper'
|
|
@@ -6,17 +7,36 @@ require_relative './rubyxl_builder'
|
|
|
6
7
|
module CSVPlusPlus
|
|
7
8
|
module Writer
|
|
8
9
|
# A class that can output a +Template+ to an Excel file
|
|
9
|
-
class Excel < ::CSVPlusPlus::Writer::
|
|
10
|
+
class Excel < ::CSVPlusPlus::Writer::Writer
|
|
11
|
+
extend ::T::Sig
|
|
10
12
|
include ::CSVPlusPlus::Writer::FileBackerUpper
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
sig { params(options: ::CSVPlusPlus::Options::FileOptions, position: ::CSVPlusPlus::Runtime::Position).void }
|
|
15
|
+
# @param options [Options::FileOptions]
|
|
16
|
+
# @param position [Runtime::Position]
|
|
17
|
+
def initialize(options, position)
|
|
18
|
+
super(position)
|
|
19
|
+
|
|
20
|
+
@options = options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { override.params(template: ::CSVPlusPlus::Template).void }
|
|
24
|
+
# Write the +template+ to an Excel file
|
|
25
|
+
#
|
|
26
|
+
# @param template [Template] The template to write
|
|
13
27
|
def write(template)
|
|
14
28
|
::CSVPlusPlus::Writer::RubyXLBuilder.new(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
options: @options,
|
|
30
|
+
position: @position,
|
|
31
|
+
rows: template.rows
|
|
18
32
|
).build_workbook.write(@options.output_filename)
|
|
19
33
|
end
|
|
34
|
+
|
|
35
|
+
sig { override.void }
|
|
36
|
+
# Write a backup of the current spreadsheet.
|
|
37
|
+
def write_backup
|
|
38
|
+
backup_file(@options)
|
|
39
|
+
end
|
|
20
40
|
end
|
|
21
41
|
end
|
|
22
42
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module CSVPlusPlus
|
|
@@ -5,32 +6,44 @@ module CSVPlusPlus
|
|
|
5
6
|
# A module that can be mixed into any Writer that needs to back up it's @output_filename (all of them except Google
|
|
6
7
|
# Sheets)
|
|
7
8
|
module FileBackerUpper
|
|
9
|
+
include ::Kernel
|
|
10
|
+
extend ::T::Sig
|
|
11
|
+
|
|
8
12
|
# I don't want to include a bunch of second/millisecond stuff in the filename unless we
|
|
9
13
|
# really need to. so try a less specifically formatted filename then get more specific
|
|
10
|
-
DESIRED_BACKUP_FORMATS =
|
|
14
|
+
DESIRED_BACKUP_FORMATS = ::T.let(
|
|
15
|
+
[
|
|
16
|
+
%(%Y_%m_%d-%I_%M%p),
|
|
17
|
+
%(%Y_%m_%d-%I_%M_%S%p),
|
|
18
|
+
%(%Y_%m_%d-%I_%M_%S_%L%p)
|
|
19
|
+
].freeze,
|
|
20
|
+
::T::Array[::String]
|
|
21
|
+
)
|
|
11
22
|
private_constant :DESIRED_BACKUP_FORMATS
|
|
12
23
|
|
|
24
|
+
sig { params(options: ::CSVPlusPlus::Options::FileOptions).returns(::T.nilable(::Pathname)) }
|
|
13
25
|
# Assuming the underlying spreadsheet is file-based, create a backup of it
|
|
14
|
-
def
|
|
15
|
-
return unless ::File.exist?(
|
|
26
|
+
def backup_file(options)
|
|
27
|
+
return unless ::File.exist?(options.output_filename)
|
|
16
28
|
|
|
17
29
|
# TODO: also don't do anything if the current backups contents haven't changed (do a md5sum or something)
|
|
18
30
|
|
|
19
|
-
attempt_backups.tap do |backed_up_to|
|
|
20
|
-
|
|
31
|
+
attempt_backups(options).tap do |backed_up_to|
|
|
32
|
+
puts("Backed up #{options.output_filename} to #{backed_up_to}") if options.verbose
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
|
|
24
36
|
private
|
|
25
37
|
|
|
38
|
+
sig { params(options: ::CSVPlusPlus::Options::FileOptions).returns(::Pathname) }
|
|
26
39
|
# rubocop:disable Metrics/MethodLength
|
|
27
|
-
def attempt_backups
|
|
40
|
+
def attempt_backups(options)
|
|
28
41
|
attempted =
|
|
29
42
|
# rubocop:disable Lint/ConstantResolution
|
|
30
43
|
DESIRED_BACKUP_FORMATS.map do |file_format|
|
|
31
44
|
# rubocop:enable Lint/ConstantResolution
|
|
32
|
-
filename = format_backup_filename(file_format)
|
|
33
|
-
backed_up_to = backup(filename)
|
|
45
|
+
filename = format_backup_filename(file_format, options.output_filename)
|
|
46
|
+
backed_up_to = backup(filename, options.output_filename)
|
|
34
47
|
|
|
35
48
|
next filename unless backed_up_to
|
|
36
49
|
|
|
@@ -44,16 +57,17 @@ module CSVPlusPlus
|
|
|
44
57
|
end
|
|
45
58
|
# rubocop:enable Metrics/MethodLength
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
sig { params(filename: ::Pathname, output_filename: ::Pathname).returns(::T.nilable(::Pathname)) }
|
|
61
|
+
def backup(filename, output_filename)
|
|
48
62
|
return if ::File.exist?(filename)
|
|
49
63
|
|
|
50
|
-
::FileUtils.cp(
|
|
64
|
+
::FileUtils.cp(output_filename, filename)
|
|
51
65
|
filename
|
|
52
66
|
end
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
sig { params(file_format: ::String, output_filename: ::Pathname).returns(::Pathname) }
|
|
69
|
+
def format_backup_filename(file_format, output_filename)
|
|
70
|
+
output_filename.sub_ext("-#{::Time.now.strftime(file_format)}" + output_filename.extname)
|
|
57
71
|
end
|
|
58
72
|
end
|
|
59
73
|
end
|
|
@@ -1,118 +1,62 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
|
-
require_relative '../google_api_client'
|
|
4
|
-
require_relative 'base_writer'
|
|
5
|
-
require_relative 'google_sheet_builder'
|
|
6
|
-
|
|
7
4
|
module CSVPlusPlus
|
|
8
5
|
module Writer
|
|
9
6
|
# A class that can write a +Template+ to Google Sheets (via their API)
|
|
10
|
-
class GoogleSheets < ::CSVPlusPlus::Writer::
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
public_constant :SPREADSHEET_INFINITY
|
|
7
|
+
class GoogleSheets < ::CSVPlusPlus::Writer::Writer
|
|
8
|
+
extend ::T::Sig
|
|
9
|
+
include ::CSVPlusPlus::GoogleApiClient
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
sig do
|
|
12
|
+
params(options: ::CSVPlusPlus::Options::GoogleSheetsOptions, position: ::CSVPlusPlus::Runtime::Position).void
|
|
13
|
+
end
|
|
14
|
+
# @param options [Options::GoogleSheetsOptions]
|
|
15
|
+
# @param position [Runtime::Position]
|
|
16
|
+
def initialize(options, position)
|
|
17
|
+
super(position)
|
|
18
18
|
|
|
19
|
-
@
|
|
20
|
-
@
|
|
19
|
+
@options = ::T.let(options, ::CSVPlusPlus::Options::GoogleSheetsOptions)
|
|
20
|
+
@reader = ::T.let(::CSVPlusPlus::Reader::GoogleSheets.new(options), ::CSVPlusPlus::Reader::GoogleSheets)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
sig { override.params(template: ::CSVPlusPlus::Template).void }
|
|
24
|
+
# Write a +template+ to Google Sheets
|
|
24
25
|
#
|
|
25
26
|
# @param template [Template]
|
|
26
27
|
def write(template)
|
|
27
|
-
@sheets_client = ::CSVPlusPlus::GoogleApiClient.sheets_client
|
|
28
|
-
|
|
29
|
-
fetch_spreadsheet!
|
|
30
|
-
fetch_spreadsheet_values!
|
|
31
|
-
|
|
32
28
|
create_sheet! if @options.create_if_not_exists
|
|
33
29
|
|
|
34
30
|
update_cells!(template)
|
|
35
31
|
end
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
sig { override.void }
|
|
34
|
+
# Write a backup of the Google Sheet that is about to be written
|
|
38
35
|
def write_backup
|
|
39
|
-
drive_client
|
|
40
|
-
drive_client.copy_file(@sheet_id)
|
|
36
|
+
drive_client.copy_file(@options.sheet_id)
|
|
41
37
|
end
|
|
42
38
|
|
|
43
39
|
private
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
@sheet_name ? "'#{@sheet_name}'!#{range}" : range
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def full_range
|
|
50
|
-
format_range('A1:Z1000')
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def fetch_spreadsheet_values!
|
|
54
|
-
formatted_values = get_all_spreadsheet_values('FORMATTED_VALUE')
|
|
55
|
-
formula_values = get_all_spreadsheet_values('FORMULA')
|
|
56
|
-
|
|
57
|
-
return if formula_values.values.nil? || formatted_values.values.nil?
|
|
58
|
-
|
|
59
|
-
@current_values = extract_current_values(formatted_values, formula_values)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def extract_current_values(formatted_values, formula_values)
|
|
63
|
-
formatted_values.values.map.each_with_index do |row, x|
|
|
64
|
-
row.map.each_with_index do |_cell, y|
|
|
65
|
-
formula_value = formula_values.values[x][y]
|
|
66
|
-
if formula_value.is_a?(::String) && formula_value.start_with?('=')
|
|
67
|
-
formula_value
|
|
68
|
-
else
|
|
69
|
-
strip_to_nil(formatted_values.values[x][y])
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def strip_to_nil(str)
|
|
76
|
-
str.strip.empty? ? nil : str
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def get_all_spreadsheet_values(render_option)
|
|
80
|
-
@sheets_client.get_spreadsheet_values(@sheet_id, full_range, value_render_option: render_option)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def sheet
|
|
84
|
-
return unless @sheet_name
|
|
85
|
-
|
|
86
|
-
@spreadsheet.sheets.find { |s| s.properties.title.strip == @sheet_name.strip }
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def fetch_spreadsheet!
|
|
90
|
-
@spreadsheet = @sheets_client.get_spreadsheet(@sheet_id)
|
|
91
|
-
|
|
92
|
-
return unless @sheet_name.nil?
|
|
93
|
-
|
|
94
|
-
@sheet_name = @spreadsheet.sheets&.first&.properties&.title
|
|
95
|
-
end
|
|
96
|
-
|
|
41
|
+
sig { void }
|
|
97
42
|
def create_sheet!
|
|
98
|
-
return if sheet
|
|
43
|
+
return if @reader.sheet
|
|
99
44
|
|
|
100
|
-
|
|
101
|
-
fetch_spreadsheet!
|
|
102
|
-
@sheet_name = @spreadsheet.sheets.last.properties.title
|
|
45
|
+
sheets_client.create_spreadsheet(@options.sheet_name)
|
|
103
46
|
end
|
|
104
47
|
|
|
48
|
+
sig { params(template: ::CSVPlusPlus::Template).void }
|
|
105
49
|
def update_cells!(template)
|
|
106
|
-
|
|
50
|
+
sheets_client.batch_update_spreadsheet(@options.sheet_id, builder(template).batch_update_spreadsheet_request)
|
|
107
51
|
end
|
|
108
52
|
|
|
53
|
+
sig { params(template: ::CSVPlusPlus::Template).returns(::CSVPlusPlus::Writer::GoogleSheetsBuilder) }
|
|
109
54
|
def builder(template)
|
|
110
|
-
::CSVPlusPlus::Writer::
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
current_sheet_values: @current_values
|
|
55
|
+
::CSVPlusPlus::Writer::GoogleSheetsBuilder.new(
|
|
56
|
+
options: @options,
|
|
57
|
+
position: @position,
|
|
58
|
+
reader: @reader,
|
|
59
|
+
rows: template.rows
|
|
116
60
|
)
|
|
117
61
|
end
|
|
118
62
|
end
|