csv_plus_plus 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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