csv_plus_plus 0.1.3 → 0.2.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -3
  3. data/docs/CHANGELOG.md +18 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +3 -3
  6. data/lib/csv_plus_plus/cell.rb +1 -35
  7. data/lib/csv_plus_plus/cli.rb +43 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +77 -70
  9. data/lib/csv_plus_plus/color.rb +1 -1
  10. data/lib/csv_plus_plus/compiler.rb +31 -21
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +11 -4
  12. data/lib/csv_plus_plus/entities/boolean.rb +16 -9
  13. data/lib/csv_plus_plus/entities/builtins.rb +68 -40
  14. data/lib/csv_plus_plus/entities/date.rb +14 -11
  15. data/lib/csv_plus_plus/entities/entity.rb +11 -29
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +18 -31
  17. data/lib/csv_plus_plus/entities/function.rb +22 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +35 -11
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +15 -10
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +36 -23
  23. data/lib/csv_plus_plus/entities/string.rb +13 -10
  24. data/lib/csv_plus_plus/entities.rb +2 -18
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +18 -5
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -13
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +10 -36
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +6 -32
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +1 -1
  33. data/lib/csv_plus_plus/error.rb +4 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +18 -8
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +53 -17
  38. data/lib/csv_plus_plus/lexer.rb +40 -1
  39. data/lib/csv_plus_plus/modifier/data_validation.rb +1 -1
  40. data/lib/csv_plus_plus/modifier/expand.rb +17 -0
  41. data/lib/csv_plus_plus/modifier.rb +6 -1
  42. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  43. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  44. data/lib/csv_plus_plus/options/options.rb +102 -0
  45. data/lib/csv_plus_plus/options.rb +22 -110
  46. data/lib/csv_plus_plus/parser/cell_value.tab.rb +65 -66
  47. data/lib/csv_plus_plus/parser/code_section.tab.rb +92 -84
  48. data/lib/csv_plus_plus/parser/modifier.tab.rb +40 -30
  49. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  50. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  51. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  52. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  53. data/lib/csv_plus_plus/reader.rb +14 -0
  54. data/lib/csv_plus_plus/runtime/graph.rb +6 -6
  55. data/lib/csv_plus_plus/runtime/{position_tracker.rb → position.rb} +16 -5
  56. data/lib/csv_plus_plus/runtime/references.rb +32 -27
  57. data/lib/csv_plus_plus/runtime/runtime.rb +73 -67
  58. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  59. data/lib/csv_plus_plus/runtime.rb +9 -9
  60. data/lib/csv_plus_plus/source_code.rb +14 -9
  61. data/lib/csv_plus_plus/template.rb +17 -12
  62. data/lib/csv_plus_plus/version.rb +1 -1
  63. data/lib/csv_plus_plus/writer/csv.rb +32 -5
  64. data/lib/csv_plus_plus/writer/excel.rb +19 -6
  65. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -14
  66. data/lib/csv_plus_plus/writer/google_sheets.rb +23 -129
  67. data/lib/csv_plus_plus/writer/{google_sheet_builder.rb → google_sheets_builder.rb} +39 -55
  68. data/lib/csv_plus_plus/writer/merger.rb +56 -0
  69. data/lib/csv_plus_plus/writer/open_document.rb +16 -2
  70. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +68 -43
  71. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  72. data/lib/csv_plus_plus/writer.rb +58 -19
  73. data/lib/csv_plus_plus.rb +26 -14
  74. metadata +43 -18
  75. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -231
  76. data/lib/csv_plus_plus/entities/variable.rb +0 -37
  77. data/lib/csv_plus_plus/error/syntax_error.rb +0 -71
  78. data/lib/csv_plus_plus/google_options.rb +0 -32
  79. data/lib/csv_plus_plus/lexer/lexer.rb +0 -89
  80. data/lib/csv_plus_plus/runtime/can_define_references.rb +0 -87
  81. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +0 -209
  82. data/lib/csv_plus_plus/writer/base_writer.rb +0 -45
@@ -1,32 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module CSVPlusPlus
5
- # The Google-specific options a user can supply.
6
- #
7
- # @attr sheet_id [String] The ID of the Google Sheet to write to.
8
- class GoogleOptions
9
- extend ::T::Sig
10
-
11
- sig { returns(::String) }
12
- attr_reader :sheet_id
13
-
14
- sig { params(sheet_id: ::String).void }
15
- # @param sheet_id [String] The unique ID Google uses to reference the sheet
16
- def initialize(sheet_id)
17
- @sheet_id = sheet_id
18
- end
19
-
20
- sig { returns(::String) }
21
- # Format a string with a verbose description of what we're doing with the options
22
- #
23
- # @return [String]
24
- def verbose_summary
25
- <<~SUMMARY
26
- ## Google Sheets Options
27
-
28
- > Sheet ID | #{@sheet_id}
29
- SUMMARY
30
- end
31
- end
32
- end
@@ -1,89 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- module CSVPlusPlus
5
- # Common methods to be mixed into the Racc parsers
6
- #
7
- # @attr_reader tokens [Array]
8
- module Lexer
9
- attr_reader :tokens
10
-
11
- # Initialize a lexer instance with an empty +@tokens+
12
- def initialize(tokens: [])
13
- @tokens = tokens
14
- end
15
-
16
- # Used by racc to iterate each token
17
- #
18
- # @return [Array<(String, String)>]
19
- def next_token
20
- @tokens.shift
21
- end
22
-
23
- # Orchestate the tokenizing, parsing and error handling of parsing input. Each instance will implement their own
24
- # #tokenizer method
25
- #
26
- # @return [Lexer#return_value] Each instance will define it's own +return_value+ with the result of parsing
27
- def parse(input, runtime)
28
- return if input.nil?
29
-
30
- return return_value unless anything_to_parse?(input)
31
-
32
- @runtime = runtime
33
-
34
- tokenize(input, runtime)
35
- do_parse
36
- return_value
37
- rescue ::Racc::ParseError => e
38
- runtime.raise_formula_syntax_error("Error parsing #{parse_subject}", e.message, wrapped_error: e)
39
- rescue ::CSVPlusPlus::Error::ModifierValidationError => e
40
- raise(::CSVPlusPlus::Error::ModifierSyntaxError.from_validation_error(runtime, e))
41
- end
42
-
43
- TOKEN_LIBRARY = {
44
- A1_NOTATION: [::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP, :A1_NOTATION],
45
- FALSE: [/false/i, :FALSE],
46
- HEX_COLOR: [::CSVPlusPlus::Color::HEX_STRING_REGEXP, :HEX_COLOR],
47
- ID: [/[$!\w:]+/, :ID],
48
- INFIX_OP: [%r{\^|\+|-|\*|/|&|<|>|<=|>=|<>}, :INFIX_OP],
49
- NUMBER: [/-?[\d.]+/, :NUMBER],
50
- STRING: [%r{"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"}, :STRING],
51
- TRUE: [/true/i, :TRUE],
52
- VAR_REF: [/\$\$/, :VAR_REF]
53
- }.freeze
54
- public_constant :TOKEN_LIBRARY
55
-
56
- private
57
-
58
- def tokenize(input, runtime)
59
- return if input.nil?
60
-
61
- t = tokenizer.scan(input)
62
-
63
- until t.scanner.empty?
64
- next if t.matches_ignore?
65
-
66
- return if t.stop?
67
-
68
- t.scan_tokens!
69
- consume_token(t, runtime)
70
- end
71
-
72
- @tokens << %i[EOL EOL]
73
- end
74
-
75
- def consume_token(tokenizer, runtime)
76
- if tokenizer.last_token
77
- @tokens << [tokenizer.last_token, tokenizer.last_match]
78
- elsif tokenizer.scan_catchall
79
- @tokens << [tokenizer.last_match, tokenizer.last_match]
80
- # TODO: checking the +parse_subject+ like this is a little hacky... but we need to know if we're parsing
81
- # modifiers or code_section (or formulas in a cell)
82
- elsif parse_subject == 'modifier'
83
- runtime.raise_modifier_syntax_error("Unable to parse #{parse_subject} starting at", tokenizer.peek)
84
- else
85
- runtime.raise_formula_syntax_error("Unable to parse #{parse_subject} starting at", tokenizer.peek)
86
- end
87
- end
88
- end
89
- end
@@ -1,87 +0,0 @@
1
- # typed: true
2
- # frozen_string_literal: true
3
-
4
- module CSVPlusPlus
5
- module Runtime
6
- # Methods for classes that need to manage +@variables+ and +@functions+
7
- module CanDefineReferences
8
- # Define a (or re-define an existing) variable
9
- #
10
- # @param id [String, Symbol] The identifier for the variable
11
- # @param entity [Entity] The value (entity) the variable holds
12
- #
13
- # @return [Entity] The value of the variable (+entity+)
14
- def def_variable(id, entity)
15
- @variables[id.to_sym] = entity
16
- end
17
-
18
- # Define (or re-define existing) variables
19
- #
20
- # @param vars [Hash<Symbol, Variable>] Variables to define
21
- def def_variables(vars)
22
- vars.each { |id, entity| def_variable(id, entity) }
23
- end
24
-
25
- # Define a (or re-define an existing) function
26
- #
27
- # @param id [String, Symbol] The identifier for the function
28
- # @param entity [Entities::Function] The defined function
29
- #
30
- # @return [Entities::Function] The defined function
31
- def def_function(id, entity)
32
- @functions[id.to_sym] = entity
33
- end
34
-
35
- # Is the variable defined?
36
- #
37
- # @param var_id [Symbol, String] The identifier of the variable
38
- #
39
- # @return [boolean]
40
- def defined_variable?(var_id)
41
- @variables.key?(var_id.to_sym)
42
- end
43
-
44
- # Is the function defined?
45
- #
46
- # @param fn_id [Symbol, String] The identifier of the function
47
- #
48
- # @return [boolean]
49
- def defined_function?(fn_id)
50
- @functions.key?(fn_id.to_sym)
51
- end
52
-
53
- # Provide a summary of the functions and variables compiled (to show in verbose mode)
54
- #
55
- # @return [String]
56
- def verbose_summary
57
- <<~SUMMARY
58
- # Code Section Summary
59
-
60
- ## Resolved Variables
61
-
62
- #{variable_summary}
63
-
64
- ## Functions
65
-
66
- #{function_summary}
67
- SUMMARY
68
- end
69
-
70
- private
71
-
72
- def variable_summary
73
- return '(no variables defined)' if @variables.empty?
74
-
75
- @variables.map { |k, v| "#{k} := #{v}" }
76
- .join("\n")
77
- end
78
-
79
- def function_summary
80
- return '(no functions defined)' if @functions.empty?
81
-
82
- @functions.map { |k, f| "#{k}: #{f}" }
83
- .join("\n")
84
- end
85
- end
86
- end
87
- end
@@ -1,209 +0,0 @@
1
- # typed: false
2
- # frozen_string_literal: true
3
-
4
- module CSVPlusPlus
5
- module Runtime
6
- # Methods for resolving functions and variables. These should be included onto a class that has +@variables+ and
7
- # +@functions+ instance variables.
8
- module CanResolveReferences
9
- # Resolve all values in the ast of the current cell being processed
10
- #
11
- # @return [Entity]
12
- def resolve_cell_value
13
- return unless (ast = @cell&.ast)
14
-
15
- last_round = nil
16
- loop do
17
- refs = ::CSVPlusPlus::Runtime::References.extract(ast, self)
18
- return ast if refs.empty?
19
-
20
- # TODO: throw an error here instead I think - basically we did a round and didn't make progress
21
- return ast if last_round == refs
22
-
23
- ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
24
- end
25
- end
26
-
27
- # Bind +var_id+ to the current cell
28
- #
29
- # @param var_id [Symbol] The name of the variable to bind the cell reference to
30
- #
31
- # @return [CellReference]
32
- def bind_variable_to_cell(var_id)
33
- def_variable(
34
- var_id,
35
- ::CSVPlusPlus::Entities::CellReference.new(
36
- cell_index: @cell_index,
37
- row_index: @row_index
38
- )
39
- )
40
- end
41
-
42
- # Bind +var_id+ relative to an ![[expand]] modifier.
43
- #
44
- # @param var_id [Symbol] The name of the variable to bind the cell reference to
45
- # @param expand [Expand] The expand where the variable is accessible (where it will be bound relative to)
46
- #
47
- # @return [CellReference]
48
- def bind_variable_in_expand(var_id, expand)
49
- def_variable(
50
- var_id,
51
- ::CSVPlusPlus::Entities::CellReference.new(
52
- scoped_to_expand: expand,
53
- cell_index: @cell_index
54
- )
55
- )
56
- end
57
-
58
- # Variables outside of an ![[expand=...] are always in scope. If it's defined within an expand then things
59
- # get trickier because the variable is only in scope while we're processing cells within that expand.
60
- #
61
- # @param var_id [Symbol] The variable's identifier that we are checking if it's in scope
62
- #
63
- # @return [boolean]
64
- def in_scope?(var_id)
65
- value = @variables[var_id]
66
-
67
- raise_modifier_syntax_error('Undefined variable reference', var_id.to_s) if value.nil?
68
-
69
- expand = value.type == ::CSVPlusPlus::Entities::Type::CellReference && value.scoped_to_expand
70
- return true unless expand
71
-
72
- unless expand.starts_at
73
- raise(::CSVPlusPlus::Error::Error, 'Must call Template.expand_rows! before checking the scope of expands.')
74
- end
75
-
76
- @row_index >= expand.starts_at && (expand.ends_at.nil? || row_index <= expand.ends_at)
77
- end
78
-
79
- private
80
-
81
- # Resolve all variable references defined statically in the code section
82
- # def resolve_static_variables!
83
- # last_var_dependencies = {}
84
- # loop do
85
- # var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
86
- # return if var_dependencies == last_var_dependencies
87
- #
88
- # # TODO: make the contract better here
89
- # @variables = resolve_dependencies(var_dependencies, resolution_order, variables)
90
- # last_var_dependencies = var_dependencies.clone
91
- # end
92
- # end
93
- #
94
- # def only_static_vars(var_dependencies)
95
- # var_dependencies.reject { |k| @runtime.builtin_variable?(k) }
96
- # end
97
-
98
- def resolve_functions(ast, refs)
99
- refs.reduce(ast.dup) do |acc, elem|
100
- function_replace(acc, elem.id, resolve_function(elem.id))
101
- end
102
- end
103
-
104
- def resolve_variables(ast, refs)
105
- refs.reduce(ast.dup) do |acc, elem|
106
- variable_replace(acc, elem.id, resolve_variable(elem.id))
107
- end
108
- end
109
-
110
- # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
111
- # rubocop:disable Metrics/MethodLength
112
- def function_replace(node, fn_id, replacement)
113
- if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall && node.id == fn_id
114
- call_function_or_builtin(replacement, node)
115
- elsif node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
116
- # not our function, but continue our depth first search on it
117
- ::CSVPlusPlus::Entities::FunctionCall.new(
118
- node.id,
119
- node.arguments.map { |n| function_replace(n, fn_id, replacement) },
120
- infix: node.infix
121
- )
122
- else
123
- node
124
- end
125
- end
126
- # rubocop:enable Metrics/MethodLength
127
-
128
- def resolve_function(fn_id)
129
- id = fn_id.to_sym
130
- return @functions[id] if defined_function?(id)
131
-
132
- ::CSVPlusPlus::Entities::Builtins::FUNCTIONS[id]
133
- end
134
-
135
- def call_function_or_builtin(function_or_builtin, function_call)
136
- if function_or_builtin.type == ::CSVPlusPlus::Entities::Type::Function
137
- call_function(function_or_builtin, function_call)
138
- else
139
- function_or_builtin.resolve_fn.call(self, function_call.arguments)
140
- end
141
- end
142
-
143
- def call_function(function, function_call)
144
- i = 0
145
- function.arguments.reduce(function.body.dup) do |ast, argument|
146
- variable_replace(ast, argument, function_call.arguments[i]).tap do
147
- i += 1
148
- end
149
- end
150
- end
151
-
152
- # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
153
- def variable_replace(node, var_id, replacement)
154
- if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
155
- arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
156
- # TODO: refactor these places where we copy functions... it's brittle with the kwargs
157
- ::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
158
- elsif node.type == ::CSVPlusPlus::Entities::Type::Variable && node.id == var_id
159
- replacement
160
- else
161
- node
162
- end
163
- end
164
-
165
- def resolve_variable(var_id)
166
- id = var_id.to_sym
167
- return @variables[id] if defined_variable?(id)
168
-
169
- raise_formula_syntax_error('Undefined variable', var_id) unless builtin_variable?(var_id)
170
-
171
- ::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
172
- end
173
-
174
- # def check_unbound_vars(dependencies, variables)
175
- # unbound_vars = dependencies.values.flatten - variables.keys
176
- # return if unbound_vars.empty?
177
- #
178
- # raise_formula_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
179
- # end
180
-
181
- # def variable_resolution_order(variables)
182
- # # we have a hash of variables => ASTs but they might have references to each other, so
183
- # # we need to interpolate them first (before interpolating the cell values)
184
- # var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
185
- # # are there any references that we don't have variables for? (undefined variable)
186
- # check_unbound_vars(var_dependencies, variables)
187
- #
188
- # # a topological sort will give us the order of dependencies
189
- # [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
190
- # # TODO: don't expose this exception directly to the caller
191
- # rescue ::TSort::Cyclic
192
- # @runtime.raise_formula_syntax_error('Cyclic variable dependency detected', var_refs.keys)
193
- # end
194
-
195
- # def resolve_dependencies(var_dependencies, resolution_order, variables)
196
- # {}.tap do |resolved_vars|
197
- # # for each var and each dependency it has, build up and mutate resolved_vars
198
- # resolution_order.each do |var|
199
- # resolved_vars[var] = variables[var].dup
200
- #
201
- # var_dependencies[var].each do |dependency|
202
- # resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
203
- # end
204
- # end
205
- # end
206
- # end
207
- end
208
- end
209
- end
@@ -1,45 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module CSVPlusPlus
5
- module Writer
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
10
- class BaseWriter
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
21
-
22
- protected
23
-
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)
30
- @options = options
31
- @runtime = runtime
32
- end
33
-
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
43
- end
44
- end
45
- end