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
@@ -1,196 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'can_define_references'
4
- require_relative 'entities'
5
- require_relative 'graph'
6
- require_relative 'references'
7
-
8
- module CSVPlusPlus
9
- # A class representing the scope of the current Template and responsible for resolving variables
10
- #
11
- # @attr_reader functions [Hash<Symbol, Function>] The currently functions defined
12
- # @attr_reader runtime [Runtime] The compiler's current runtime
13
- # @attr_reader variables [Hash<Symbol, Entity>] The currently defined variables
14
- #
15
- # rubocop:disable Metrics/ClassLength
16
- class Scope
17
- include ::CSVPlusPlus::CanDefineReferences
18
- # TODO: split out a CanResolveReferences
19
-
20
- attr_reader :functions, :runtime, :variables
21
-
22
- # @param runtime [Runtime]
23
- def initialize(runtime:, functions: {}, variables: {})
24
- @runtime = runtime
25
- @functions = functions
26
- @variables = variables
27
- end
28
-
29
- # Resolve all values in the ast of the current cell being processed
30
- #
31
- # @return [Entity]
32
- def resolve_cell_value
33
- return unless (ast = @runtime.cell&.ast)
34
-
35
- last_round = nil
36
- loop do
37
- refs = ::CSVPlusPlus::References.extract(ast, self)
38
- return ast if refs.empty?
39
-
40
- # TODO: throw an error here instead I think - basically we did a round and didn't make progress
41
- return ast if last_round == refs
42
-
43
- ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
44
- end
45
- end
46
-
47
- # Bind +var_id+ to the current cell (based on where +@runtime+ is currently pointing).
48
- #
49
- # @param var_id [Symbol] The name of the variable to bind the cell reference to
50
- #
51
- # @return [CellReference]
52
- def bind_variable_to_cell(var_id)
53
- ::CSVPlusPlus::Entities::CellReference.from_index(
54
- cell_index: runtime.cell_index,
55
- row_index: runtime.row_index
56
- ).tap do |cell_reference|
57
- def_variable(var_id, cell_reference)
58
- end
59
- end
60
-
61
- # @return [String]
62
- def to_s
63
- "Scope(functions: #{@functions}, runtime: #{@runtime}, variables: #{@variables})"
64
- end
65
-
66
- private
67
-
68
- # Resolve all variable references defined statically in the code section
69
- # TODO: experiment with getting rid of this - does it even play correctly with runtime vars?
70
- def resolve_static_variables!
71
- last_var_dependencies = {}
72
- loop do
73
- var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
74
- return if var_dependencies == last_var_dependencies
75
-
76
- # TODO: make the contract better here
77
- @variables = resolve_dependencies(var_dependencies, resolution_order, variables)
78
- last_var_dependencies = var_dependencies.clone
79
- end
80
- end
81
-
82
- def only_static_vars(var_dependencies)
83
- var_dependencies.reject { |k| @runtime.runtime_variable?(k) }
84
- end
85
-
86
- def resolve_functions(ast, refs)
87
- refs.reduce(ast.dup) do |acc, elem|
88
- function_replace(acc, elem.id, resolve_function(elem.id))
89
- end
90
- end
91
-
92
- def resolve_variables(ast, refs)
93
- refs.reduce(ast.dup) do |acc, elem|
94
- variable_replace(acc, elem.id, resolve_variable(elem.id))
95
- end
96
- end
97
-
98
- # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
99
- # rubocop:disable Metrics/MethodLength
100
- def function_replace(node, fn_id, replacement)
101
- if node.function_call? && node.id == fn_id
102
- call_function_or_runtime_value(replacement, node)
103
- elsif node.function_call?
104
- # not our function, but continue our depth first search on it
105
- ::CSVPlusPlus::Entities::FunctionCall.new(
106
- node.id,
107
- node.arguments.map { |n| function_replace(n, fn_id, replacement) },
108
- infix: node.infix
109
- )
110
- else
111
- node
112
- end
113
- end
114
- # rubocop:enable Metrics/MethodLength
115
-
116
- def resolve_function(fn_id)
117
- id = fn_id.to_sym
118
- return functions[id] if defined_function?(id)
119
-
120
- ::CSVPlusPlus::Entities::Builtins::FUNCTIONS[id]
121
- end
122
-
123
- def call_function_or_runtime_value(function_or_runtime_value, function_call)
124
- if function_or_runtime_value.function?
125
- call_function(function_or_runtime_value, function_call)
126
- else
127
- function_or_runtime_value.resolve_fn.call(@runtime, function_call.arguments)
128
- end
129
- end
130
-
131
- def call_function(function, function_call)
132
- i = 0
133
- function.arguments.reduce(function.body.dup) do |ast, argument|
134
- variable_replace(ast, argument, function_call.arguments[i]).tap do
135
- i += 1
136
- end
137
- end
138
- end
139
-
140
- # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
141
- def variable_replace(node, var_id, replacement)
142
- if node.function_call?
143
- arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
144
- # TODO: refactor these places where we copy functions... it's brittle with the kwargs
145
- ::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
146
- elsif node.variable? && node.id == var_id
147
- replacement
148
- else
149
- node
150
- end
151
- end
152
-
153
- def resolve_variable(var_id)
154
- id = var_id.to_sym
155
- return variables[id] if defined_variable?(id)
156
-
157
- # this will throw a syntax error if it doesn't exist (which is what we want)
158
- @runtime.runtime_value(id)
159
- end
160
-
161
- def check_unbound_vars(dependencies, variables)
162
- unbound_vars = dependencies.values.flatten - variables.keys
163
- return if unbound_vars.empty?
164
-
165
- @runtime.raise_formula_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
166
- end
167
-
168
- def variable_resolution_order(variables)
169
- # we have a hash of variables => ASTs but they might have references to each other, so
170
- # we need to interpolate them first (before interpolating the cell values)
171
- var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
172
- # are there any references that we don't have variables for? (undefined variable)
173
- check_unbound_vars(var_dependencies, variables)
174
-
175
- # a topological sort will give us the order of dependencies
176
- [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
177
- # TODO: don't expose this exception directly to the caller
178
- rescue ::TSort::Cyclic
179
- @runtime.raise_formula_syntax_error('Cyclic variable dependency detected', var_refs.keys)
180
- end
181
-
182
- def resolve_dependencies(var_dependencies, resolution_order, variables)
183
- {}.tap do |resolved_vars|
184
- # for each var and each dependency it has, build up and mutate resolved_vars
185
- resolution_order.each do |var|
186
- resolved_vars[var] = variables[var].dup
187
-
188
- var_dependencies[var].each do |dependency|
189
- resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
190
- end
191
- end
192
- end
193
- end
194
- end
195
- # rubocop:enable Metrics/ClassLength
196
- end
@@ -1,164 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'data_validation'
4
- require_relative 'modifier'
5
-
6
- module CSVPlusPlus
7
- # Validates and coerces modifier values as they are parsed.
8
- #
9
- # Previously this logic was handled in the parser's grammar, but with the introduction of variable binding, the
10
- # grammar is no longer context free so we need the parser to be a little looser on what it accepts and validate it
11
- # here. Having this layer is also nice because we can provide better error messages to the user for what went
12
- # wrong during the parse.
13
- class ValidatedModifier < ::CSVPlusPlus::Modifier
14
- # Validates that +border+ is 'all', 'top', 'bottom', 'left' or 'right'.
15
- #
16
- # @param value [String] The unvalidated user input
17
- def border=(value)
18
- super(one_of(:border, value, %i[all top bottom left right]))
19
- end
20
-
21
- # Validates that +bordercolor+ is a hex color.
22
- #
23
- # @param value [String] The unvalidated user input
24
- def bordercolor=(value)
25
- super(color_value(:bordercolor, value))
26
- end
27
-
28
- # Validates that +borderstyle+ is 'dashed', 'dotted', 'double', 'solid', 'solid_medium' or 'solid_thick'.
29
- #
30
- # @param value [String] The unvalidated user input
31
- def borderstyle=(value)
32
- super(one_of(:borderstyle, value, %i[dashed dotted double solid solid_medium solid_thick]))
33
- end
34
-
35
- # Validates that +color+ is a hex color.
36
- #
37
- # @param value [String] The unvalidated user input
38
- def color=(value)
39
- super(color_value(:color, value))
40
- end
41
-
42
- # Validates that +expand+ is a positive integer.
43
- #
44
- # @param value [String] The unvalidated user input
45
- def expand=(value)
46
- super(::CSVPlusPlus::Expand.new(positive_integer(:expand, value)))
47
- end
48
-
49
- # Validates that +fontcolor+ is a hex color.
50
- #
51
- # @param value [String] The unvalidated user input
52
- def fontcolor=(value)
53
- super(color_value(:fontcolor, value))
54
- end
55
-
56
- # Validates that +fontcolor+ is a hex color.
57
- def fontfamily=(value)
58
- super(matches_regexp(:fontfamily, unquote(value), /^[\w\s]+$/, 'It is not a valid font family.'))
59
- end
60
-
61
- # Validates that +fontsize+ is a positive integer
62
- #
63
- # @param value [String] The unvalidated user input
64
- def fontsize=(value)
65
- super(positive_integer(:fontsize, value))
66
- end
67
-
68
- # Validates that +format+ is 'bold', 'italic', 'strikethrough' or 'underline'.
69
- #
70
- # @param value [String] The unvalidated user input
71
- def format=(value)
72
- super(one_of(:format, value, %i[bold italic strikethrough underline]))
73
- end
74
-
75
- # Validates that +halign+ is 'left', 'center' or 'right'.
76
- #
77
- # @param value [String] The unvalidated user input
78
- def halign=(value)
79
- super(one_of(:halign, value, %i[left center right]))
80
- end
81
-
82
- # Validates that +note+ is a quoted string.
83
- #
84
- # @param value [String] The unvalidated user input
85
-
86
- # Validates that +numberformat+ is 'currency', 'date', 'date_time', 'number', 'percent', 'text', 'time' or
87
- # 'scientific'.
88
- #
89
- # @param value [String] The unvalidated user input
90
- def numberformat=(value)
91
- super(one_of(:nubmerformat, value, %i[currency date date_time number percent text time scientific]))
92
- end
93
-
94
- # Validates that +valign+ is 'top', 'center' or 'bottom'.
95
- #
96
- # @param value [String] The unvalidated user input
97
- def valign=(value)
98
- super(one_of(:valign, value, %i[top center bottom]))
99
- end
100
-
101
- # Validates that the conditional validating rules are well-formed.
102
- #
103
- # Pretty much based off of the Google Sheets API spec here:
104
- #
105
- # @param value [String] The unvalidated user input
106
- def validation=(value)
107
- super(a_data_validation(:validation, value))
108
- end
109
-
110
- # Validates +variable+ is a valid variable identifier.
111
- #
112
- # @param value [String] The unvalidated user input
113
- def var=(value)
114
- # TODO: I need a shared definition of what a variable can be (I guess the :ID token)
115
- super(matches_regexp(:var, value, /^\w+$/, 'It must be a sequence of letters, numbers and _.').to_sym)
116
- end
117
-
118
- private
119
-
120
- # XXX centralize this :(((
121
- def unquote(str)
122
- # TODO: I'm pretty sure this isn't sufficient and we need to deal with the backslashes
123
- str.gsub(/^['\s]*|['\s]*$/, '')
124
- end
125
-
126
- def a_data_validation(modifier, value)
127
- data_validation = ::CSVPlusPlus::DataValidation.new(value)
128
- return data_validation unless data_validation.valid?
129
-
130
- raise_error(modifier, value, message: data_validation.invalid_reason)
131
- end
132
-
133
- def color_value(modifier, value)
134
- unless ::CSVPlusPlus::Color.valid_hex_string?(value)
135
- raise_error(modifier, value, message: 'It must be a 3 or 6 digit hex code.')
136
- end
137
-
138
- ::CSVPlusPlus::Color.new(value)
139
- end
140
-
141
- def matches_regexp(modifier, value, regexp, message)
142
- raise_error(modifier, value, message:) unless value =~ regexp
143
- value
144
- end
145
-
146
- def one_of(modifier, value, choices)
147
- value.downcase.to_sym.tap do |v|
148
- raise_error(modifier, value, choices:) unless choices.include?(v)
149
- end
150
- end
151
-
152
- def positive_integer(modifier, value)
153
- Integer(value, 10).tap do |i|
154
- raise_error(modifier, value, message: 'It must be positive and greater than 0.') unless i.positive?
155
- end
156
- rescue ::ArgumentError
157
- raise_error(modifier, value, message: 'It must be a valid (whole) number.')
158
- end
159
-
160
- def raise_error(modifier, bad_input, choices: nil, message: nil)
161
- raise(::CSVPlusPlus::Error::ModifierValidationError.new(modifier, bad_input, choices:, message:))
162
- end
163
- end
164
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Writer
5
- # Decorate a Modifier so it can be written to the Google Sheets API
6
- class GoogleSheetModifier < ::SimpleDelegator
7
- # Format the border for Google Sheets
8
- #
9
- # @return [Google::Apis::SheetsV4::Border]
10
- def border
11
- # TODO: allow different border styles per side
12
- ::Google::Apis::SheetsV4::Border.new(
13
- color: bordercolor&.to_s || '#000000',
14
- style: borderstyle&.to_s || 'solid'
15
- )
16
- end
17
-
18
- # Format the color for Google Sheets
19
- #
20
- # @return [Google::Apis::SheetsV4::Color]
21
- def color
22
- google_sheets_color(super) if super
23
- end
24
-
25
- # Format the fontcolor for Google Sheets
26
- #
27
- # @return [Google::Apis::SheetsV4::Color]
28
- def fontcolor
29
- google_sheets_color(super) if super
30
- end
31
-
32
- # Format the halign for Google Sheets
33
- #
34
- # @return [String]
35
- def halign
36
- super&.to_s&.upcase
37
- end
38
-
39
- # Format the numberformat for Google Sheets
40
- #
41
- # @return [::Google::Apis::SheetsV4::NumberFormat]
42
- def numberformat
43
- ::Google::Apis::SheetsV4::NumberFormat.new(type: super) if super
44
- end
45
-
46
- # Builds a SheetsV4::TextFormat with the underlying Modifier
47
- #
48
- # @return [::Google::Apis::SheetsV4::TextFormat]
49
- def text_format
50
- ::Google::Apis::SheetsV4::TextFormat.new(
51
- bold: formatted?(:bold) || nil,
52
- italic: formatted?(:italic) || nil,
53
- strikethrough: formatted?(:strikethrough) || nil,
54
- underline: formatted?(:underline) || nil,
55
- font_family: fontfamily,
56
- font_size: fontsize,
57
- foreground_color: fontcolor
58
- )
59
- end
60
-
61
- # Format the valign for Google Sheets
62
- def valign
63
- super&.to_s&.upcase
64
- end
65
-
66
- private
67
-
68
- def google_sheets_color(color)
69
- ::Google::Apis::SheetsV4::Color.new(
70
- red: color.red_percent,
71
- green: color.green_percent,
72
- blue: color.blue_percent
73
- )
74
- end
75
- end
76
- end
77
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Writer
5
- # Build a RubyXL-decorated Modifier class adds some support for Excel
6
- class RubyXLModifier < ::SimpleDelegator
7
- # https://www.rubydoc.info/gems/rubyXL/RubyXL/NumberFormats
8
- # https://support.microsoft.com/en-us/office/number-format-codes-5026bbd6-04bc-48cd-bf33-80f18b4eae68
9
- NUM_FMT_IDS = {
10
- currency: 5,
11
- date: 14,
12
- date_time: 22,
13
- number: 1,
14
- percent: 9,
15
- text: 49,
16
- time: 21,
17
- scientific: 48
18
- }.freeze
19
- private_constant :NUM_FMT_IDS
20
-
21
- # https://www.rubydoc.info/gems/rubyXL/2.3.0/RubyXL
22
- # ST_BorderStyle = %w{ none thin medium dashed dotted thick double hair mediumDashed dashDot mediumDashDot
23
- # dashDotDot slantDashDot }
24
- BORDER_STYLES = {
25
- dashed: 'dashed',
26
- dotted: 'dotted',
27
- double: 'double',
28
- solid: 'thin',
29
- solid_medium: 'medium',
30
- solid_thick: 'thick'
31
- }.freeze
32
- private_constant :BORDER_STYLES
33
-
34
- # The excel-specific border weight
35
- #
36
- # @return [Integer]
37
- def border_weight
38
- return unless borderstyle
39
-
40
- # rubocop:disable Lint/ConstantResolution
41
- BORDER_STYLES[borderstyle.to_sym]
42
- # rubocop:enable Lint/ConstantResolution
43
- end
44
-
45
- # The excel-specific number format code
46
- #
47
- # @return [String]
48
- def number_format_code
49
- return unless numberformat
50
-
51
- ::RubyXL::NumberFormats::DEFAULT_NUMBER_FORMATS.find_by_format_id(
52
- # rubocop:disable Lint/ConstantResolution
53
- NUM_FMT_IDS[numberformat.to_sym]
54
- # rubocop:enable Lint/ConstantResolution
55
- ).format_code
56
- end
57
- end
58
- end
59
- end