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
|
@@ -7,32 +7,32 @@
|
|
|
7
7
|
require 'racc/parser.rb'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
require_relative '../
|
|
11
|
-
require_relative '../lexer'
|
|
10
|
+
require_relative '../lexer/racc_lexer'
|
|
12
11
|
|
|
13
12
|
module CSVPlusPlus
|
|
14
13
|
module Parser
|
|
15
14
|
class Modifier < Racc::Parser
|
|
16
15
|
|
|
17
|
-
module_eval(<<'...end modifier.y/module_eval...', 'modifier.y',
|
|
18
|
-
|
|
16
|
+
module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 60)
|
|
17
|
+
extend ::T::Sig
|
|
18
|
+
extend ::T::Generic
|
|
19
|
+
include ::CSVPlusPlus::Lexer::RaccLexer
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
ReturnType = type_member {{ fixed: ::T.nilable(::String) }}
|
|
21
22
|
|
|
22
23
|
# @param cell_modifier [Modifier]
|
|
23
24
|
# @param row_modifier [Modifier]
|
|
24
|
-
|
|
25
|
-
def initialize(cell_modifier:, row_modifier:, scope:)
|
|
25
|
+
def initialize(cell_modifier:, row_modifier:)
|
|
26
26
|
super()
|
|
27
27
|
|
|
28
28
|
@parsing_row = false
|
|
29
|
-
@cell_modifier = cell_modifier
|
|
30
|
-
@row_modifier = row_modifier
|
|
31
|
-
@scope = scope
|
|
29
|
+
@cell_modifier = ::CSVPlusPlus::Modifier::ModifierValidator.new(cell_modifier)
|
|
30
|
+
@row_modifier = ::CSVPlusPlus::Modifier::ModifierValidator.new(row_modifier)
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
protected
|
|
35
34
|
|
|
35
|
+
sig { override.params(input: ::String).returns(::T::Boolean) }
|
|
36
36
|
def anything_to_parse?(input)
|
|
37
37
|
@modifiers_to_parse = input.scan(/!?\[\[/).count
|
|
38
38
|
|
|
@@ -44,10 +44,18 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
|
|
|
44
44
|
@modifiers_to_parse > 0
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
sig { override.returns(::String) }
|
|
47
48
|
def parse_subject
|
|
48
49
|
'modifier'
|
|
49
50
|
end
|
|
50
51
|
|
|
52
|
+
sig { override.returns(ReturnType) }
|
|
53
|
+
# The output of the parser
|
|
54
|
+
def return_value
|
|
55
|
+
@return_value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { override.returns(::CSVPlusPlus::Lexer::Tokenizer) }
|
|
51
59
|
def tokenizer
|
|
52
60
|
::CSVPlusPlus::Lexer::Tokenizer.new(
|
|
53
61
|
ignore: /\s+/,
|
|
@@ -61,42 +69,43 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
|
|
|
61
69
|
@modifiers_to_parse == 0
|
|
62
70
|
end,
|
|
63
71
|
tokens: [
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/
|
|
72
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bborder\b/, token: 'border'),
|
|
73
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bbordercolor\b/, token: 'bordercolor'),
|
|
74
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bborderstyle\b/, token: 'borderstyle'),
|
|
75
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bcolor\b/, token: 'color'),
|
|
76
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bexpand\b/, token: 'expand'),
|
|
77
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bfontcolor\b/, token: 'fontcolor'),
|
|
78
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bfontfamily\b/, token: 'fontfamily'),
|
|
79
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bfontsize\b/, token: 'fontsize'),
|
|
80
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bformat\b/, token: 'format'),
|
|
81
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bfreeze\b/, token: 'freeze'),
|
|
82
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bhalign\b/, token: 'halign'),
|
|
83
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bnote\b/, token: 'note'),
|
|
84
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bnumberformat\b/, token: 'numberformat'),
|
|
85
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bvalidate\b/, token: 'validate'),
|
|
86
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bvalign\b/, token: 'valign'),
|
|
87
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\bvar\b/, token: 'var'),
|
|
88
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /-?[1-9][\d.]*/, token: :NUMBER),
|
|
89
|
+
::CSVPlusPlus::Lexer::Token.new(
|
|
90
|
+
regexp: /
|
|
84
91
|
(?:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
|
92
|
+
\w+\s*:\s*'([^'\\]|\\.)*') # allow for a single-quoted string which can accept any input and also allow
|
|
93
|
+
# for escaping via backslash (i.e., 'ain\\'t won\\'t something' is valid)
|
|
94
|
+
| # - or -
|
|
95
|
+
(?:'([^'\\]|\\.)*') # allow for a single-quoted string which can accept any input and also allow
|
|
96
|
+
|
|
|
90
97
|
(?:
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
[\w,_:-] # something that accepts most basic input if it doesn't need to be quoted
|
|
99
|
+
[\w\s,_:-]+ # same thing but allow spaces in the middle
|
|
100
|
+
[\w,_:-] # no spaces at the end
|
|
101
|
+
)
|
|
93
102
|
/x,
|
|
94
|
-
:RIGHT_SIDE,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
token: :RIGHT_SIDE,
|
|
104
|
+
),
|
|
105
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\[\[/, token: :START_CELL_MODIFIERS),
|
|
106
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /!\[\[/, token: :START_ROW_MODIFIERS),
|
|
107
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /\//, token: :MODIFIER_SEPARATOR),
|
|
108
|
+
::CSVPlusPlus::Lexer::Token.new(regexp: /=/, token: :EQ),
|
|
100
109
|
],
|
|
101
110
|
alter_matches: {
|
|
102
111
|
STRING: ->(s) { s.gsub(/^'|'$/, '') }
|
|
@@ -107,7 +116,7 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
|
|
|
107
116
|
private
|
|
108
117
|
|
|
109
118
|
def assign_defaults!
|
|
110
|
-
@cell_modifier.take_defaults_from!(@row_modifier)
|
|
119
|
+
@cell_modifier.modifier.take_defaults_from!(@row_modifier.modifier)
|
|
111
120
|
end
|
|
112
121
|
|
|
113
122
|
def parsing_row!
|
|
@@ -123,11 +132,6 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
|
|
|
123
132
|
assign_defaults!
|
|
124
133
|
end
|
|
125
134
|
|
|
126
|
-
def define_var(var_id)
|
|
127
|
-
@scope.bind_variable_to_cell(var_id)
|
|
128
|
-
modifier.var = var_id.to_sym
|
|
129
|
-
end
|
|
130
|
-
|
|
131
135
|
def modifier
|
|
132
136
|
@parsing_row ? @row_modifier : @cell_modifier
|
|
133
137
|
end
|
|
@@ -160,7 +164,7 @@ racc_action_pointer = [
|
|
|
160
164
|
36, 52, 41, nil, nil, nil, 56, nil, -17, -1,
|
|
161
165
|
nil, 41, nil, 49, 50, 51, 52, 53, 54, 55,
|
|
162
166
|
56, 57, nil, 58, 59, 60, 61, 62, 63, 42,
|
|
163
|
-
nil, 15, 59,
|
|
167
|
+
nil, 15, 59, 60, 61, 62, 66, 64, 65, 69,
|
|
164
168
|
67, 68, 69, 70, 71, 72, 73, nil, nil, nil,
|
|
165
169
|
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
|
166
170
|
nil, nil, nil, nil ]
|
|
@@ -393,7 +397,7 @@ module_eval(<<'.,.,', 'modifier.y', 39)
|
|
|
393
397
|
|
|
394
398
|
module_eval(<<'.,.,', 'modifier.y', 40)
|
|
395
399
|
def _reduce_15(val, _values, result)
|
|
396
|
-
modifier.
|
|
400
|
+
modifier.infinite_expand!
|
|
397
401
|
result
|
|
398
402
|
end
|
|
399
403
|
.,.,
|
|
@@ -456,7 +460,7 @@ module_eval(<<'.,.,', 'modifier.y', 48)
|
|
|
456
460
|
|
|
457
461
|
module_eval(<<'.,.,', 'modifier.y', 49)
|
|
458
462
|
def _reduce_24(val, _values, result)
|
|
459
|
-
modifier.
|
|
463
|
+
modifier.validate = val[2]
|
|
460
464
|
result
|
|
461
465
|
end
|
|
462
466
|
.,.,
|
|
@@ -470,7 +474,7 @@ module_eval(<<'.,.,', 'modifier.y', 50)
|
|
|
470
474
|
|
|
471
475
|
module_eval(<<'.,.,', 'modifier.y', 51)
|
|
472
476
|
def _reduce_26(val, _values, result)
|
|
473
|
-
|
|
477
|
+
modifier.var = val[2]
|
|
474
478
|
result
|
|
475
479
|
end
|
|
476
480
|
.,.,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Reader
|
|
6
|
+
# Reads a CSV file
|
|
7
|
+
class CSV < ::CSVPlusPlus::Reader::Reader
|
|
8
|
+
extend ::T::Sig
|
|
9
|
+
extend ::T::Generic
|
|
10
|
+
|
|
11
|
+
CellValue = type_member { { fixed: ::String } }
|
|
12
|
+
public_constant :CellValue
|
|
13
|
+
|
|
14
|
+
sig { params(options: ::CSVPlusPlus::Options::FileOptions).void }
|
|
15
|
+
# Open a CSV outputter to the +output_filename+ specified by the +Options+
|
|
16
|
+
#
|
|
17
|
+
# @param options [Options] The supplied options.
|
|
18
|
+
def initialize(options)
|
|
19
|
+
super()
|
|
20
|
+
|
|
21
|
+
@options = options
|
|
22
|
+
@cell_values = ::T.let(
|
|
23
|
+
read_csv(@options.output_filename),
|
|
24
|
+
::T::Array[::T::Array[::T.nilable(::CSVPlusPlus::Reader::CSV::CellValue)]]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { override.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::CSV::CellValue)) }
|
|
29
|
+
# Get the current value at the +cell+'s location.
|
|
30
|
+
#
|
|
31
|
+
# @param cell [Cell]
|
|
32
|
+
#
|
|
33
|
+
# @return [CellValue, nil]
|
|
34
|
+
def value_at(cell)
|
|
35
|
+
@cell_values[cell.row_index]&.[](cell.index)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
sig do
|
|
41
|
+
params(filename: ::Pathname).returns(::T::Array[::T::Array[::T.nilable(::CSVPlusPlus::Reader::CSV::CellValue)]])
|
|
42
|
+
end
|
|
43
|
+
def read_csv(filename)
|
|
44
|
+
return [[]] unless ::File.exist?(filename)
|
|
45
|
+
|
|
46
|
+
::CSV.read(filename.to_s)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Reader
|
|
6
|
+
# A class that can read an existing Google Sheets spreadsheet
|
|
7
|
+
class GoogleSheets < ::CSVPlusPlus::Reader::Reader
|
|
8
|
+
extend ::T::Sig
|
|
9
|
+
extend ::T::Generic
|
|
10
|
+
include ::CSVPlusPlus::GoogleApiClient
|
|
11
|
+
|
|
12
|
+
CellValue = type_member { { fixed: ::String } }
|
|
13
|
+
public_constant :CellValue
|
|
14
|
+
|
|
15
|
+
sig { params(options: ::CSVPlusPlus::Options::GoogleSheetsOptions).void }
|
|
16
|
+
# Open a CSV outputter to the +output_filename+ specified by the +Options+
|
|
17
|
+
#
|
|
18
|
+
# @param options [Options::GoogleSheetsOptions] The supplied options.
|
|
19
|
+
def initialize(options)
|
|
20
|
+
super()
|
|
21
|
+
|
|
22
|
+
@options = options
|
|
23
|
+
@cell_values = ::T.let(nil, ::T.nilable(::T::Array[::T::Array[::T.nilable(::String)]]))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig do
|
|
27
|
+
override.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::GoogleSheets::CellValue))
|
|
28
|
+
end
|
|
29
|
+
# Get the current value at the +cell+'s location.
|
|
30
|
+
#
|
|
31
|
+
# @param cell [Cell]
|
|
32
|
+
#
|
|
33
|
+
# @return [CellValue, nil]
|
|
34
|
+
def value_at(cell)
|
|
35
|
+
cell_values[cell.row_index]&.[](cell.index)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { returns(::T.nilable(::Google::Apis::SheetsV4::Sheet)) }
|
|
39
|
+
# @return [Google::Apis::SheetsV4::Sheet, nil]
|
|
40
|
+
def sheet
|
|
41
|
+
spreadsheet.sheets.find { |s| s.properties.title.strip == @options.sheet_name.strip }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { returns(::Google::Apis::SheetsV4::Spreadsheet) }
|
|
45
|
+
# @return [Google::Apis::SheetsV4::Spreadsheet]
|
|
46
|
+
def spreadsheet
|
|
47
|
+
@spreadsheet ||= ::T.let(
|
|
48
|
+
sheets_client.get_spreadsheet(@options.sheet_id),
|
|
49
|
+
::T.nilable(::Google::Apis::SheetsV4::Spreadsheet)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
unless @spreadsheet
|
|
53
|
+
raise(::CSVPlusPlus::Error::WriterError, "Unable to connect to google spreadsheet #{@options.sheet_id}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@spreadsheet
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
sig { returns(::T::Array[::T::Array[::T.nilable(::String)]]) }
|
|
62
|
+
# rubocop:disable Metrics/MethodLength
|
|
63
|
+
def cell_values
|
|
64
|
+
return @cell_values if @cell_values
|
|
65
|
+
|
|
66
|
+
formatted_values = get_all_spreadsheet_values('FORMATTED_VALUE')
|
|
67
|
+
formula_values = get_all_spreadsheet_values('FORMULA')
|
|
68
|
+
|
|
69
|
+
@cell_values =
|
|
70
|
+
::T.must(
|
|
71
|
+
::T.let(
|
|
72
|
+
if formula_values&.values.nil? || formatted_values&.values.nil?
|
|
73
|
+
[]
|
|
74
|
+
else
|
|
75
|
+
extract_current_values(::T.must(formatted_values), ::T.must(formula_values))
|
|
76
|
+
end,
|
|
77
|
+
::T.nilable(::T::Array[::T::Array[::T.nilable(::String)]])
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
# rubocop:enable Metrics/MethodLength
|
|
82
|
+
|
|
83
|
+
sig { params(render_option: ::String).returns(::T.nilable(::Google::Apis::SheetsV4::ValueRange)) }
|
|
84
|
+
def get_all_spreadsheet_values(render_option)
|
|
85
|
+
sheets_client.get_spreadsheet_values(@options.sheet_id, full_range, value_render_option: render_option)
|
|
86
|
+
rescue ::Google::Apis::ClientError => e
|
|
87
|
+
return if e.status_code == 404
|
|
88
|
+
|
|
89
|
+
raise
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sig do
|
|
93
|
+
params(
|
|
94
|
+
formatted_values: ::Google::Apis::SheetsV4::ValueRange,
|
|
95
|
+
formula_values: ::Google::Apis::SheetsV4::ValueRange
|
|
96
|
+
).returns(::T::Array[::T::Array[::T.nilable(::String)]])
|
|
97
|
+
end
|
|
98
|
+
def extract_current_values(formatted_values, formula_values)
|
|
99
|
+
formatted_values.values.map.each_with_index do |row, x|
|
|
100
|
+
row.map.each_with_index do |_cell, y|
|
|
101
|
+
formula_value = formula_values.values[x][y]
|
|
102
|
+
if formula_value.is_a?(::String) && formula_value.start_with?('=')
|
|
103
|
+
formula_value
|
|
104
|
+
else
|
|
105
|
+
strip_to_nil(formatted_values.values[x][y])
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
sig { params(range: ::String).returns(::String) }
|
|
112
|
+
def format_range(range)
|
|
113
|
+
# "'#{@options.sheet_name}'!#{range}"
|
|
114
|
+
# XXX
|
|
115
|
+
range
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sig { returns(::String) }
|
|
119
|
+
def full_range
|
|
120
|
+
format_range('A1:Z1000')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
sig { params(str: ::String).returns(::T.nilable(::String)) }
|
|
124
|
+
def strip_to_nil(str)
|
|
125
|
+
str.strip.empty? ? nil : str
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Reader
|
|
6
|
+
# +Reader+s are used to complement +Writer+ instances by reading in the existing spreadsheet and providing
|
|
7
|
+
# a way to merge in results.
|
|
8
|
+
class Reader
|
|
9
|
+
extend ::T::Sig
|
|
10
|
+
extend ::T::Generic
|
|
11
|
+
extend ::T::Helpers
|
|
12
|
+
|
|
13
|
+
abstract!
|
|
14
|
+
|
|
15
|
+
CellValue = type_member
|
|
16
|
+
public_constant :CellValue
|
|
17
|
+
|
|
18
|
+
sig { abstract.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::Reader::CellValue)) }
|
|
19
|
+
# Get the current value at the +cell+'s location.
|
|
20
|
+
#
|
|
21
|
+
# @param cell [Cell]
|
|
22
|
+
#
|
|
23
|
+
# @return [CellValue, nil]
|
|
24
|
+
def value_at(cell); end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
module Reader
|
|
6
|
+
# Reads an Excel file
|
|
7
|
+
class RubyXL < ::CSVPlusPlus::Reader::Reader
|
|
8
|
+
extend ::T::Sig
|
|
9
|
+
extend ::T::Generic
|
|
10
|
+
|
|
11
|
+
CellValue = type_member { { fixed: ::RubyXL::Cell } }
|
|
12
|
+
public_constant :CellValue
|
|
13
|
+
|
|
14
|
+
sig { params(options: ::CSVPlusPlus::Options::FileOptions, worksheet: ::RubyXL::Worksheet).void }
|
|
15
|
+
# Open an excel outputter to the +output_filename+ specified by the +Options+
|
|
16
|
+
#
|
|
17
|
+
# @param options [Options] The supplied options.
|
|
18
|
+
# @param worksheet [RubyXL::Worksheet] The already-opened RubyXL worksheet
|
|
19
|
+
def initialize(options, worksheet)
|
|
20
|
+
super()
|
|
21
|
+
|
|
22
|
+
@options = options
|
|
23
|
+
@worksheet = worksheet
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::RubyXL::CellValue)) }
|
|
27
|
+
# Get the current value at the +cell+'s position
|
|
28
|
+
#
|
|
29
|
+
# @param cell [Cell]
|
|
30
|
+
#
|
|
31
|
+
# @return [RubyXL::Cell, nil]
|
|
32
|
+
def value_at(cell)
|
|
33
|
+
@worksheet.sheet_data[cell.row_index]&.[](cell.index)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CSVPlusPlus
|
|
5
|
+
# Classes which can read spreadsheets in our various formats.
|
|
6
|
+
module Reader
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative './reader/reader'
|
|
11
|
+
|
|
12
|
+
require_relative './reader/csv'
|
|
13
|
+
require_relative './reader/google_sheets'
|
|
14
|
+
require_relative './reader/rubyxl'
|
data/lib/csv_plus_plus/row.rb
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
|
+
# typed: strict
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module CSVPlusPlus
|
|
4
|
-
# A row of a template
|
|
5
|
+
# A row of a template. A row contains an +Array+ of +Cell+s and possibly a row-level +Modifier+.
|
|
5
6
|
#
|
|
6
|
-
# @attr_reader cells [Array<Cell>]
|
|
7
|
-
# @attr_reader index [Integer] The index of this row
|
|
7
|
+
# @attr_reader cells [Array<Cell>] The cells contained by this row.
|
|
8
|
+
# @attr_reader index [Integer] The index of this row. Starts at 0.
|
|
8
9
|
# @attr_reader modifier [Modifier] The modifier to apply to all cells in this row
|
|
9
10
|
class Row
|
|
10
|
-
|
|
11
|
+
extend ::T::Sig
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
sig { returns(::T::Array[::CSVPlusPlus::Cell]) }
|
|
14
|
+
attr_reader :cells
|
|
15
|
+
|
|
16
|
+
sig { returns(::Integer) }
|
|
17
|
+
attr_reader :index
|
|
18
|
+
|
|
19
|
+
sig { returns(::CSVPlusPlus::Modifier::Modifier) }
|
|
20
|
+
attr_reader :modifier
|
|
21
|
+
|
|
22
|
+
sig do
|
|
23
|
+
params(cells: ::T::Array[::CSVPlusPlus::Cell], index: ::Integer, modifier: ::CSVPlusPlus::Modifier::Modifier).void
|
|
24
|
+
end
|
|
13
25
|
# @param cells [Array<Cell>] The cells belonging to this row
|
|
26
|
+
# @param index [Integer] The index of this row (starts at 0)
|
|
14
27
|
# @param modifier [Modifier] The modifier to apply to all cells in this row
|
|
15
|
-
def initialize(index
|
|
28
|
+
def initialize(cells:, index:, modifier:)
|
|
16
29
|
@cells = cells
|
|
17
30
|
@modifier = modifier
|
|
18
31
|
@index = index
|
|
19
32
|
end
|
|
20
33
|
|
|
34
|
+
sig { params(index: ::Integer).void }
|
|
21
35
|
# Set the row's +index+ and update the +row_index+ of all affected cells
|
|
22
36
|
#
|
|
23
37
|
# @param index [Integer] The index of this row (starts at 0)
|
|
@@ -26,25 +40,52 @@ module CSVPlusPlus
|
|
|
26
40
|
@cells.each { |cell| cell.row_index = index }
|
|
27
41
|
end
|
|
28
42
|
|
|
43
|
+
sig { returns(::Integer) }
|
|
29
44
|
# How much this row will expand itself, if at all (0)
|
|
30
45
|
#
|
|
31
46
|
# @return [Integer]
|
|
32
47
|
def expand_amount
|
|
33
|
-
return 0
|
|
48
|
+
return 0 if @modifier.expand.nil?
|
|
34
49
|
|
|
35
|
-
@modifier.expand.repetitions || (1000 - @index)
|
|
50
|
+
::T.must(@modifier.expand).repetitions || (1000 - @index)
|
|
36
51
|
end
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
sig { params(starts_at: ::Integer, into: ::T::Array[::CSVPlusPlus::Row]).returns(::T::Array[::CSVPlusPlus::Row]) }
|
|
54
|
+
# Starting at +starts_at+, do a deep copy of this row into the +Array+ referenced by +into+.
|
|
55
|
+
#
|
|
56
|
+
# @param starts_at [Integer] The +row_index+ where this row was expanded.
|
|
57
|
+
# @param into [Array<Row>] An array where the expanded rows will be accumulated.
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Row>] The rows expanded
|
|
60
|
+
def expand_rows(starts_at:, into: [])
|
|
61
|
+
return into if @modifier.expand.nil?
|
|
62
|
+
|
|
63
|
+
::T.must(@modifier.expand).starts_at = starts_at
|
|
64
|
+
|
|
65
|
+
starts_at.upto(expand_amount + starts_at - 1) do |row_index|
|
|
66
|
+
into << deep_clone.tap { |c| c.index = row_index }
|
|
67
|
+
end
|
|
68
|
+
into
|
|
41
69
|
end
|
|
42
70
|
|
|
71
|
+
sig { returns(::T::Boolean) }
|
|
72
|
+
# Does the row have an ![[expand]] modifier but is yet to be expanded?
|
|
73
|
+
#
|
|
74
|
+
# @return [T::Boolean]
|
|
75
|
+
def unexpanded?
|
|
76
|
+
return false if @modifier.expand.nil?
|
|
77
|
+
|
|
78
|
+
!::T.must(@modifier.expand).expanded?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
sig { returns(::CSVPlusPlus::Row) }
|
|
43
84
|
# Return a deep copy of this row
|
|
44
85
|
#
|
|
45
86
|
# @return [Row]
|
|
46
87
|
def deep_clone
|
|
47
|
-
::Marshal.load(::Marshal.dump(self))
|
|
88
|
+
::T.cast(::Marshal.load(::Marshal.dump(self)), ::CSVPlusPlus::Row)
|
|
48
89
|
end
|
|
49
90
|
end
|
|
50
91
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'tsort'
|
|
5
|
+
|
|
6
|
+
module CSVPlusPlus
|
|
7
|
+
module Runtime
|
|
8
|
+
# Graph ordering and searching functions
|
|
9
|
+
module Graph
|
|
10
|
+
# Get a list of all variables references in a given +ast+
|
|
11
|
+
# TODO: this is only used in one place - refactor it
|
|
12
|
+
def self.variable_references(ast, include_runtime_variables: false)
|
|
13
|
+
depth_first_search(ast) do |node|
|
|
14
|
+
next unless node.is_a?(::CSVPlusPlus::Entities::Reference)
|
|
15
|
+
|
|
16
|
+
node.id if !::CSVPlusPlus::Entities::Builtins.builtin_variable?(node.id) || include_runtime_variables
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create a dependency graph of +variables+
|
|
21
|
+
def self.dependency_graph(variables)
|
|
22
|
+
::CSVPlusPlus::Runtime::Graph::DependencyGraph[
|
|
23
|
+
variables.map { |var_id, ast| [var_id, variable_references(ast)] }
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# TODO: I don't think we use this anymore - it was useful when I wanted to resolve variables in their dependency
|
|
28
|
+
# order
|
|
29
|
+
#
|
|
30
|
+
# Perform a topological sort on a +DependencyGraph+. A toplogical sort is noteworthy
|
|
31
|
+
# because it will give us the order in which we need to resolve our variable dependencies.
|
|
32
|
+
#
|
|
33
|
+
# Given this dependency graph:
|
|
34
|
+
#
|
|
35
|
+
# { a: [b c], b: [c], c: [d], d: [] }
|
|
36
|
+
#
|
|
37
|
+
# it will return:
|
|
38
|
+
#
|
|
39
|
+
# [d, c, b, a]
|
|
40
|
+
#
|
|
41
|
+
def self.topological_sort(dependencies)
|
|
42
|
+
dependencies.tsort
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Do a DFS on an AST starting at +node+
|
|
46
|
+
def self.depth_first_search(node, accum = [], &)
|
|
47
|
+
ret = yield(node)
|
|
48
|
+
accum << ret unless ret.nil?
|
|
49
|
+
|
|
50
|
+
return accum unless node.is_a?(::CSVPlusPlus::Entities::FunctionCall)
|
|
51
|
+
|
|
52
|
+
node.arguments.each { |n| depth_first_search(n, accum, &) }
|
|
53
|
+
accum
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A dependency graph represented as a +Hash+ which will be used by our +topological_sort+ function
|
|
57
|
+
class DependencyGraph < Hash
|
|
58
|
+
include ::TSort
|
|
59
|
+
alias tsort_each_node each_key
|
|
60
|
+
|
|
61
|
+
# sort each child
|
|
62
|
+
def tsort_each_child(node, &)
|
|
63
|
+
fetch(node).each(&)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|