csv_plus_plus 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/docs/CHANGELOG.md +16 -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 +71 -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 +97 -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 +31 -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 +37 -12
  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