csv_plus_plus 0.1.2 → 0.1.3

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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +9 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +23 -13
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +32 -7
  9. data/lib/csv_plus_plus/compiler.rb +82 -60
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +27 -43
  11. data/lib/csv_plus_plus/entities/boolean.rb +18 -9
  12. data/lib/csv_plus_plus/entities/builtins.rb +23 -9
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +200 -29
  14. data/lib/csv_plus_plus/entities/date.rb +38 -5
  15. data/lib/csv_plus_plus/entities/entity.rb +27 -61
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +23 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +24 -9
  19. data/lib/csv_plus_plus/entities/number.rb +24 -10
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +22 -5
  21. data/lib/csv_plus_plus/entities/string.rb +19 -6
  22. data/lib/csv_plus_plus/entities/variable.rb +16 -4
  23. data/lib/csv_plus_plus/entities.rb +20 -13
  24. data/lib/csv_plus_plus/error/error.rb +11 -1
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +1 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +53 -5
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +34 -14
  28. data/lib/csv_plus_plus/error/syntax_error.rb +22 -9
  29. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  30. data/lib/csv_plus_plus/error.rb +1 -0
  31. data/lib/csv_plus_plus/google_api_client.rb +7 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +8 -4
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -158
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/parser/cell_value.tab.rb +5 -5
  46. data/lib/csv_plus_plus/parser/code_section.tab.rb +8 -13
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +17 -23
  48. data/lib/csv_plus_plus/row.rb +53 -12
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +34 -191
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +62 -35
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +1 -0
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +71 -23
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -30
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +29 -12
  69. metadata +18 -14
  70. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  71. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  72. data/lib/csv_plus_plus/data_validation.rb +0 -138
  73. data/lib/csv_plus_plus/expand.rb +0 -20
  74. data/lib/csv_plus_plus/graph.rb +0 -62
  75. data/lib/csv_plus_plus/references.rb +0 -68
  76. data/lib/csv_plus_plus/scope.rb +0 -196
  77. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  78. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  79. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,126 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc) for parsing
7
+ # a given file. We take multiple runs through the input file for parsing so it's really convenient to have a
8
+ # central place for these things to be managed.
9
+ #
10
+ # @attr_reader filename [String, nil] The filename that the input came from (mostly used for debugging since
11
+ # +filename+ can be +nil+ if it's read from stdin.
12
+ #
13
+ # @attr cell [Cell] The current cell being processed
14
+ # @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
15
+ # @attr row_index [Integer] The index of the current row being processed (starts at 0)
16
+ # @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
17
+ class Runtime
18
+ extend ::T::Sig
19
+
20
+ include ::CSVPlusPlus::Runtime::CanDefineReferences
21
+ include ::CSVPlusPlus::Runtime::CanResolveReferences
22
+ include ::CSVPlusPlus::Runtime::PositionTracker
23
+
24
+ sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function]) }
25
+ attr_reader :functions
26
+
27
+ sig { returns(::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]) }
28
+ attr_reader :variables
29
+
30
+ sig { returns(::CSVPlusPlus::SourceCode) }
31
+ attr_reader :source_code
32
+
33
+ sig do
34
+ params(
35
+ source_code: ::CSVPlusPlus::SourceCode,
36
+ functions: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function],
37
+ variables: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]
38
+ ).void
39
+ end
40
+ # @param source_code [SourceCode] The source code being compiled
41
+ # @param functions [Hash<Symbol, Function>] Pre-defined functions
42
+ # @param variables [Hash<Symbol, Entity>] Pre-defined variables
43
+ def initialize(source_code:, functions: {}, variables: {})
44
+ @functions = functions
45
+ @variables = variables
46
+ @source_code = source_code
47
+
48
+ rewrite_input!(source_code.input)
49
+ end
50
+
51
+ sig { params(fn_id: ::Symbol).returns(::T::Boolean) }
52
+ # Is +fn_id+ a builtin function?
53
+ #
54
+ # @param fn_id [Symbol] The Function#id to check if it's a runtime variable
55
+ #
56
+ # @return [T::Boolean]
57
+ def builtin_function?(fn_id)
58
+ ::CSVPlusPlus::Entities::Builtins::FUNCTIONS.key?(fn_id)
59
+ end
60
+
61
+ sig { params(var_id: ::Symbol).returns(::T::Boolean) }
62
+ # Is +var_id+ a builtin variable?
63
+ #
64
+ # @param var_id [Symbol] The Variable#id to check if it's a runtime variable
65
+ #
66
+ # @return [T::Boolean]
67
+ def builtin_variable?(var_id)
68
+ ::CSVPlusPlus::Entities::Builtins::VARIABLES.key?(var_id)
69
+ end
70
+
71
+ sig { returns(::T::Boolean) }
72
+ # Is the parser currently inside of the code section? (includes the `---`)
73
+ #
74
+ # @return [T::Boolean]
75
+ def parsing_code_section?
76
+ source_code.in_code_section?(line_number)
77
+ end
78
+
79
+ sig { returns(::T::Boolean) }
80
+ # Is the parser currently inside of the CSV section?
81
+ #
82
+ # @return [T::Boolean]
83
+ def parsing_csv_section?
84
+ source_code.in_csv_section?(line_number)
85
+ end
86
+
87
+ sig do
88
+ params(message: ::String, bad_input: ::String, wrapped_error: ::T.nilable(::StandardError))
89
+ .returns(::T.noreturn)
90
+ end
91
+ # Called when an error is encoutered during parsing formulas (whether in the code section or a cell). It will
92
+ # construct a useful error with the current +@row/@cell_index+, +@line_number+ and +@filename+
93
+ #
94
+ # @param message [::String] A message relevant to why this error is being raised.
95
+ # @param bad_input [::String] The offending input that caused this error to be thrown.
96
+ # @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
97
+ def raise_formula_syntax_error(message, bad_input, wrapped_error: nil)
98
+ raise(::CSVPlusPlus::Error::FormulaSyntaxError.new(message, bad_input, self, wrapped_error:))
99
+ end
100
+
101
+ sig do
102
+ params(message: ::String, bad_input: ::String, wrapped_error: ::T.nilable(::StandardError))
103
+ .returns(::T.noreturn)
104
+ end
105
+ # Called when an error is encountered while parsing a modifier.
106
+ #
107
+ # @param message [::String] A message relevant to why this error is being raised.
108
+ # @param bad_input [::String] The offending input that caused this error to be thrown.
109
+ # @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
110
+ def raise_modifier_syntax_error(message, bad_input, wrapped_error: nil)
111
+ raise(::CSVPlusPlus::Error::ModifierSyntaxError.new(self, bad_input:, message:, wrapped_error:))
112
+ end
113
+
114
+ sig do
115
+ type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
116
+ end
117
+ # Reset the runtime state starting at the CSV section
118
+ # rubocop:disable Naming/BlockForwarding
119
+ def start_at_csv!(&block)
120
+ self.line_number = source_code.length_of_code_section + 1
121
+ start!(&block)
122
+ end
123
+ # rubocop:enable Naming/BlockForwarding
124
+ end
125
+ end
126
+ end
@@ -1,199 +1,42 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
4
+ require_relative './runtime/can_define_references'
5
+ require_relative './runtime/can_resolve_references'
6
+ require_relative './runtime/graph'
7
+ require_relative './runtime/position_tracker'
8
+ require_relative './runtime/references'
9
+ require_relative './runtime/runtime'
10
+
3
11
  module CSVPlusPlus
4
- # The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc) for parsing
5
- # a given file. We take multiple runs through the input file for parsing so it's really convenient to have a
6
- # central place for these things to be managed.
12
+ # All functionality needed to keep track of the runtime AKA execution context. This module has a lot of
13
+ # reponsibilities:
7
14
  #
8
- # @attr_reader filename [String, nil] The filename that the input came from (mostly used for debugging since
9
- # +filename+ can be +nil+ if it's read from stdin.
10
- # @attr_reader length_of_code_section [Integer] The length (count of lines) of the code section part of the original
11
- # input.
12
- # @attr_reader length_of_csv_section [Integer] The length (count of lines) of the CSV part of the original csvpp
13
- # input.
14
- # @attr_reader length_of_original_file [Integer] The length (count of lines) of the original csvpp input.
15
+ # - variables and function resolution and scoping
16
+ # - variable & function definitions
17
+ # - keeping track of the runtime state (the current cell being processed)
18
+ # - rewriting the input file that's being parsed
15
19
  #
16
- # @attr cell [Cell] The current cell being processed
17
- # @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
18
- # @attr row_index [Integer] The index of the current row being processed (starts at 0)
19
- # @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
20
- class Runtime
21
- attr_reader :filename, :length_of_code_section, :length_of_csv_section, :length_of_original_file
22
-
23
- attr_accessor :cell, :cell_index, :row_index, :line_number
24
-
25
- # @param input [String] The input to be parsed
26
- # @param filename [String, nil] The filename that the input came from (mostly used for debugging since +filename+
27
- # can be +nil+ if it's read from stdin
28
- def initialize(input:, filename:)
29
- @filename = filename || 'stdin'
30
-
31
- init_input!(input)
32
- start!
33
- end
34
-
35
- # Map over an a csvpp file and keep track of line_number and row_index
36
- #
37
- # @param lines [Array]
38
- #
39
- # @return [Array]
40
- def map_lines(lines, &block)
41
- @line_number = 1
42
- lines.map do |line|
43
- block.call(line).tap { next_line! }
44
- end
45
- end
46
-
47
- # Map over a single row and keep track of the cell and it's index
48
- #
49
- # @param row [Array<Cell>] The row to map each cell over
50
- #
51
- # @return [Array]
52
- def map_row(row, &block)
53
- @cell_index = 0
54
- row.map.with_index do |cell, index|
55
- set_cell!(cell, index)
56
- block.call(cell, index)
57
- end
58
- end
59
-
60
- # Map over all rows and keep track of row and line numbers
61
- #
62
- # @param rows [Array<Row>] The rows to map over (and keep track of indexes)
63
- # @param cells_too [boolean] If the cells of each +row+ should be iterated over also.
64
- #
65
- # @return [Array]
66
- def map_rows(rows, cells_too: false, &block)
67
- @row_index = 0
68
- map_lines(rows) do |row|
69
- if cells_too
70
- # it's either CSV or a Row object
71
- map_row((row.is_a?(::CSVPlusPlus::Row) ? row.cells : row), &block)
72
- else
73
- block.call(row)
74
- end
75
- end
76
- end
77
-
78
- # Increment state to the next line
79
- #
80
- # @return [Integer]
81
- def next_line!
82
- @row_index += 1 unless @row_index.nil?
83
- @line_number += 1
84
- end
85
-
86
- # Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
87
- #
88
- # @return [Integer, nil]
89
- def rownum
90
- return if @row_index.nil?
91
-
92
- @row_index + 1
93
- end
94
-
95
- # Set the current cell and index
96
- #
97
- # @param cell [Cell] The current cell
98
- # @param cell_index [Integer] The index of the cell
99
- def set_cell!(cell, cell_index)
100
- @cell = cell
101
- @cell_index = cell_index
102
- end
103
-
104
- # Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
105
- def start!
106
- @row_index = @cell_index = nil
107
- @line_number = 1
108
- end
109
-
110
- # Reset the runtime state starting at the CSV section
111
- def start_at_csv!
112
- # TODO: isn't the input re-written anyway without the code section? why do we need this?
113
- start!
114
- @line_number = @length_of_code_section || 1
115
- end
116
-
117
- # @return [String]
118
- def to_s
119
- "Runtime(cell: #{@cell}, row_index: #{@row_index}, cell_index: #{@cell_index})"
120
- end
121
-
122
- # Get the current (entity) value of a runtime value
123
- #
124
- # @param var_id [String, Symbol] The Variable#id of the variable being resolved.
125
- #
126
- # @return [Entity]
127
- def runtime_value(var_id)
128
- if runtime_variable?(var_id)
129
- ::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
130
- else
131
- raise_formula_syntax_error('Undefined variable', var_id)
132
- end
133
- end
134
-
135
- # Is +var_id+ a runtime variable? (it's a static variable otherwise)
136
- #
137
- # @param var_id [String, Symbol] The Variable#id to check if it's a runtime variable
138
- #
139
- # @return [boolean]
140
- def runtime_variable?(var_id)
141
- ::CSVPlusPlus::Entities::Builtins::VARIABLES.key?(var_id.to_sym)
142
- end
143
-
144
- # Called when an error is encoutered during parsing. It will construct a useful
145
- # error with the current +@row/@cell_index+, +@line_number+ and +@filename+
146
- #
147
- # @param message [String] A message relevant to why this error is being raised.
148
- # @param bad_input [String] The offending input that caused this error to be thrown.
149
- # @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
150
- def raise_formula_syntax_error(message, bad_input, wrapped_error: nil)
151
- raise(::CSVPlusPlus::Error::FormulaSyntaxError.new(message, bad_input, self, wrapped_error:))
152
- end
153
-
154
- # The currently available input for parsing. The tmp state will be re-written
155
- # between parsing the code section and the CSV section
156
- #
157
- # @return [String]
158
- def input
159
- @tmp
160
- end
161
-
162
- # We mutate the input over and over. It's ok because it's just a Tempfile
163
- #
164
- # @param data [String] The data to rewrite our input file to
165
- def rewrite_input!(data)
166
- @tmp.truncate(0)
167
- @tmp.write(data)
168
- @tmp.rewind
169
- end
170
-
171
- # Clean up the Tempfile we're using for parsing
172
- def cleanup!
173
- return unless @tmp
174
-
175
- @tmp.close
176
- @tmp.unlink
177
- @tmp = nil
178
- end
179
-
180
- private
181
-
182
- def count_code_section_lines(lines)
183
- eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
184
- lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
185
- end
186
-
187
- def init_input!(input)
188
- lines = (input || '').split(/\s*\n\s*/)
189
- @length_of_original_file = lines.length
190
- @length_of_code_section = count_code_section_lines(lines)
191
- @length_of_csv_section = @length_of_original_file - @length_of_code_section
192
-
193
- # we're gonna take our input file, write it to a tmp file then each
194
- # step is gonna mutate that tmp file
195
- @tmp = ::Tempfile.new
196
- rewrite_input!(input)
20
+ module Runtime
21
+ extend ::T::Sig
22
+
23
+ sig do
24
+ params(
25
+ source_code: ::CSVPlusPlus::SourceCode,
26
+ functions: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Function],
27
+ variables: ::T::Hash[::Symbol, ::CSVPlusPlus::Entities::Entity]
28
+ ).returns(::CSVPlusPlus::Runtime::Runtime)
29
+ end
30
+ # Initialize a runtime instance with all the functionality we need. A runtime is one-to-one with a file being
31
+ # compiled.
32
+ #
33
+ # @param source_code [SourceCode] The csv++ source code to be compiled
34
+ # @param functions [Hash<Symbol, Function>] Pre-defined functions
35
+ # @param variables [Hash<Symbol, Entity>] Pre-defined variables
36
+ #
37
+ # @return [Runtime::Runtime]
38
+ def self.new(source_code:, functions: {}, variables: {})
39
+ ::CSVPlusPlus::Runtime::Runtime.new(source_code:, functions:, variables:)
197
40
  end
198
41
  end
199
42
  end
@@ -0,0 +1,66 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # Information about the unparsed source code
6
+ class SourceCode
7
+ extend ::T::Sig
8
+
9
+ sig { returns(::String) }
10
+ attr_reader :input
11
+
12
+ sig { returns(::String) }
13
+ attr_reader :filename
14
+
15
+ sig { returns(::Integer) }
16
+ attr_reader :length_of_csv_section
17
+
18
+ sig { returns(::Integer) }
19
+ attr_reader :length_of_code_section
20
+
21
+ sig { returns(::Integer) }
22
+ attr_reader :length_of_file
23
+
24
+ sig { params(input: ::String, filename: ::T.nilable(::String)).void }
25
+ # @param input [::String] The source code being parsed
26
+ # @param filename [::String, nil] The name of the file the source came from. If not set we assume it came
27
+ # from stdin
28
+ def initialize(input:, filename: nil)
29
+ @input = input
30
+ @filename = ::T.let(filename || 'stdin', ::String)
31
+
32
+ lines = input.split(/[\r\n]/)
33
+ @length_of_file = ::T.let(lines.length, ::Integer)
34
+ @length_of_code_section = ::T.let(count_code_section_lines(lines), ::Integer)
35
+ @length_of_csv_section = ::T.let(@length_of_file - @length_of_code_section, ::Integer)
36
+ end
37
+
38
+ sig { params(line_number: ::Integer).returns(::T::Boolean) }
39
+ # Does the given +line_number+ land in the code section of the file? (which includes the --- separator)
40
+ #
41
+ # @param line_number [Integer]
42
+ #
43
+ # @return [T::Boolean]
44
+ def in_code_section?(line_number)
45
+ line_number <= @length_of_code_section
46
+ end
47
+
48
+ sig { params(line_number: ::Integer).returns(::T::Boolean) }
49
+ # Does the given +line_number+ land in the CSV section of the file?
50
+ #
51
+ # @param line_number [Integer]
52
+ #
53
+ # @return [T::Boolean]
54
+ def in_csv_section?(line_number)
55
+ line_number > @length_of_code_section
56
+ end
57
+
58
+ private
59
+
60
+ sig { params(lines: ::T::Array[::String]).returns(::Integer) }
61
+ def count_code_section_lines(lines)
62
+ eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
63
+ lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
64
+ end
65
+ end
66
+ end
@@ -1,42 +1,70 @@
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 scope [Scope] The +Scope+ containing all function and variable references
8
+ # @attr_reader runtime [Runtime] The +Runtime+ containing all function and variable references
8
9
  class Template
9
- attr_reader :rows, :scope
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 scope [Scope] The +Scope+ containing all function and variable references
13
- def initialize(rows:, scope:)
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
- # @return [String]
19
- def to_s
20
- "Template(rows: #{@rows}, scope: #{@scope})"
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 a +CellReference+ 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
+ def bind_all_vars!(runtime)
36
+ runtime.map_rows(@rows) do |row|
37
+ # rubocop:disable Style/MissingElse
38
+ if row.unexpanded?
39
+ # rubocop:enable Style/MissingElse
40
+ raise(::CSVPlusPlus::Error::Error, 'Template#expand_rows! must be called before Template#bind_all_vars!')
41
+ end
42
+
43
+ runtime.map_row(row.cells) do |cell|
44
+ bind_vars(cell, row.modifier.expand)
45
+ end
46
+ end
21
47
  end
22
48
 
23
- # Apply any expand= modifiers to the parsed template
49
+ sig { returns(::T::Array[::CSVPlusPlus::Row]) }
50
+ # Apply expand= (adding rows to the results) modifiers to the parsed template. This happens in towards the end of
51
+ # compilation because expanding rows will change the relative rownums as rows are added, and variables can't be
52
+ # bound until the rows have been assigned their final rownums.
24
53
  #
25
54
  # @return [Array<Row>]
26
55
  def expand_rows!
27
- expanded_rows = []
28
- row_index = 0
29
- expand_rows(
30
- lambda do |new_row|
31
- new_row.index = row_index
32
- expanded_rows << new_row
33
- row_index += 1
56
+ # TODO: make it so that an infinite expand will not overwrite the rows below it, but instead merge with them
57
+ @rows =
58
+ rows.reduce([]) do |expanded_rows, row|
59
+ if row.modifier.expand
60
+ row.expand_rows(starts_at: expanded_rows.length, into: expanded_rows)
61
+ else
62
+ expanded_rows << row.tap { |r| r.index = expanded_rows.length }
63
+ end
34
64
  end
35
- )
36
-
37
- @rows = expanded_rows
38
65
  end
39
66
 
67
+ sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).void }
40
68
  # Make sure that the template has a valid amount of infinite expand modifiers
41
69
  #
42
70
  # @param runtime [Runtime] The compiler's current runtime
@@ -44,19 +72,20 @@ module CSVPlusPlus
44
72
  infinite_expand_rows = @rows.filter { |r| r.modifier.expand&.infinite? }
45
73
  return unless infinite_expand_rows.length > 1
46
74
 
47
- runtime.raise_formula_syntax_error(
75
+ runtime.raise_modifier_syntax_error(
48
76
  'You can only have one infinite expand= (on all others you must specify an amount)',
49
- infinite_expand_rows[1]
77
+ infinite_expand_rows[1].to_s
50
78
  )
51
79
  end
52
80
 
53
- # Provide a summary of the state of the template (and it's +@scope+)
81
+ sig { returns(::String) }
82
+ # Provide a summary of the state of the template (and it's +@runtime+)
54
83
  #
55
- # @return [String]
84
+ # @return [::String]
56
85
  def verbose_summary
57
86
  # TODO: we can probably include way more stats in here
58
87
  <<~SUMMARY
59
- #{@scope.verbose_summary}
88
+ #{@runtime.verbose_summary}
60
89
 
61
90
  > #{@rows.length} rows to be written
62
91
  SUMMARY
@@ -64,17 +93,15 @@ module CSVPlusPlus
64
93
 
65
94
  private
66
95
 
67
- def expand_rows(push_row_fn)
68
- # TODO: make it so that an infinite expand will not overwrite the rows below it, but
69
- # instead merge with them
70
- rows.each do |row|
71
- if row.modifier.expand
72
- row.expand_amount.times do
73
- push_row_fn.call(row.deep_clone)
74
- end
75
- else
76
- push_row_fn.call(row)
77
- end
96
+ sig { params(cell: ::CSVPlusPlus::Cell, expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)).void }
97
+ def bind_vars(cell, expand)
98
+ var = cell.modifier.var
99
+ return unless var
100
+
101
+ if expand
102
+ @runtime.bind_variable_in_expand(var, expand)
103
+ else
104
+ @runtime.bind_variable_to_cell(var)
78
105
  end
79
106
  end
80
107
  end
@@ -1,6 +1,7 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
- VERSION = '0.1.2'
5
+ VERSION = '0.1.3'
5
6
  public_constant :VERSION
6
7
  end
@@ -1,20 +1,45 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
5
  module Writer
5
6
  # Some shared functionality that all Writers should build on
7
+ #
8
+ # @attr_reader options [Options] The supplied options - some of which are relevant for our writer instance
9
+ # @attr_reader runtime [Runtime] The current runtime - needed to resolve variables and display useful error messages
6
10
  class BaseWriter
7
- attr_accessor :options
11
+ extend ::T::Sig
12
+ extend ::T::Helpers
13
+
14
+ abstract!
15
+
16
+ sig { returns(::CSVPlusPlus::Options) }
17
+ attr_reader :options
18
+
19
+ sig { returns(::CSVPlusPlus::Runtime::Runtime) }
20
+ attr_reader :runtime
8
21
 
9
22
  protected
10
23
 
11
- # Open a CSV outputter to +filename+
12
- def initialize(options)
24
+ sig { params(options: ::CSVPlusPlus::Options, runtime: ::CSVPlusPlus::Runtime::Runtime).void }
25
+ # Open a CSV outputter to the +output_filename+ specified by the +Options+
26
+ #
27
+ # @param options [Options] The supplied options.
28
+ # @param runtime [Runtime] The current runtime.
29
+ def initialize(options, runtime)
13
30
  @options = options
14
- load_requires
31
+ @runtime = runtime
15
32
  end
16
33
 
17
- def load_requires; end
34
+ sig { abstract.params(template: ::CSVPlusPlus::Template).void }
35
+ # Write the given +template+.
36
+ #
37
+ # @param template [Template]
38
+ def write(template); end
39
+
40
+ sig { abstract.void }
41
+ # Write a backup of the current spreadsheet.
42
+ def write_backup; end
18
43
  end
19
44
  end
20
45
  end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative './file_backer_upper'
@@ -6,28 +7,29 @@ module CSVPlusPlus
6
7
  module Writer
7
8
  # A class that can output a +Template+ to CSV
8
9
  class CSV < ::CSVPlusPlus::Writer::BaseWriter
10
+ extend ::T::Sig
11
+
9
12
  include ::CSVPlusPlus::Writer::FileBackerUpper
10
13
 
11
- # write a +template+ to CSV
14
+ sig { override.params(template: ::CSVPlusPlus::Template).void }
15
+ # Write a +template+ to CSV
16
+ #
17
+ # @param template [Template] The template to use as input to be written. It should have been compiled by calling
18
+ # Compiler#compile_template
12
19
  def write(template)
13
20
  # TODO: also read it and merge the results
14
21
  ::CSV.open(@options.output_filename, 'wb') do |csv|
15
- template.rows.each do |row|
22
+ @runtime.map_rows(template.rows) do |row|
16
23
  csv << build_row(row)
17
24
  end
18
25
  end
19
26
  end
20
27
 
21
- protected
22
-
23
- def load_requires
24
- require('csv')
25
- end
26
-
27
28
  private
28
29
 
30
+ sig { params(row: ::CSVPlusPlus::Row).returns(::T::Array[::T.nilable(::String)]) }
29
31
  def build_row(row)
30
- row.cells.map(&:to_csv)
32
+ @runtime.map_row(row.cells) { |cell, _i| cell.evaluate(@runtime) }
31
33
  end
32
34
  end
33
35
  end