csv_plus_plus 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/README.md +18 -62
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +62 -0
  5. data/lib/csv_plus_plus/can_define_references.rb +88 -0
  6. data/lib/csv_plus_plus/can_resolve_references.rb +8 -0
  7. data/lib/csv_plus_plus/cell.rb +3 -3
  8. data/lib/csv_plus_plus/cli.rb +24 -7
  9. data/lib/csv_plus_plus/color.rb +12 -6
  10. data/lib/csv_plus_plus/compiler.rb +156 -0
  11. data/lib/csv_plus_plus/data_validation.rb +138 -0
  12. data/lib/csv_plus_plus/{language → entities}/ast_builder.rb +5 -7
  13. data/lib/csv_plus_plus/entities/boolean.rb +31 -0
  14. data/lib/csv_plus_plus/{language → entities}/builtins.rb +2 -4
  15. data/lib/csv_plus_plus/entities/cell_reference.rb +60 -0
  16. data/lib/csv_plus_plus/entities/date.rb +30 -0
  17. data/lib/csv_plus_plus/entities/entity.rb +84 -0
  18. data/lib/csv_plus_plus/entities/function.rb +33 -0
  19. data/lib/csv_plus_plus/entities/function_call.rb +35 -0
  20. data/lib/csv_plus_plus/entities/number.rb +34 -0
  21. data/lib/csv_plus_plus/entities/runtime_value.rb +26 -0
  22. data/lib/csv_plus_plus/entities/string.rb +29 -0
  23. data/lib/csv_plus_plus/entities/variable.rb +25 -0
  24. data/lib/csv_plus_plus/entities.rb +33 -0
  25. data/lib/csv_plus_plus/error/error.rb +10 -0
  26. data/lib/csv_plus_plus/error/formula_syntax_error.rb +36 -0
  27. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +27 -0
  28. data/lib/csv_plus_plus/error/modifier_validation_error.rb +49 -0
  29. data/lib/csv_plus_plus/{language → error}/syntax_error.rb +6 -14
  30. data/lib/csv_plus_plus/error/writer_error.rb +9 -0
  31. data/lib/csv_plus_plus/error.rb +9 -2
  32. data/lib/csv_plus_plus/expand.rb +3 -1
  33. data/lib/csv_plus_plus/google_api_client.rb +4 -0
  34. data/lib/csv_plus_plus/lexer/lexer.rb +19 -11
  35. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +17 -0
  36. data/lib/csv_plus_plus/modifier.rb +73 -70
  37. data/lib/csv_plus_plus/options.rb +3 -0
  38. data/lib/csv_plus_plus/parser/cell_value.tab.rb +305 -0
  39. data/lib/csv_plus_plus/parser/code_section.tab.rb +410 -0
  40. data/lib/csv_plus_plus/parser/modifier.tab.rb +484 -0
  41. data/lib/csv_plus_plus/references.rb +68 -0
  42. data/lib/csv_plus_plus/row.rb +0 -3
  43. data/lib/csv_plus_plus/runtime.rb +199 -0
  44. data/lib/csv_plus_plus/scope.rb +196 -0
  45. data/lib/csv_plus_plus/template.rb +21 -5
  46. data/lib/csv_plus_plus/validated_modifier.rb +164 -0
  47. data/lib/csv_plus_plus/version.rb +1 -1
  48. data/lib/csv_plus_plus/writer/file_backer_upper.rb +6 -4
  49. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +24 -29
  50. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +33 -12
  51. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +3 -6
  52. data/lib/csv_plus_plus.rb +41 -16
  53. metadata +34 -24
  54. data/lib/csv_plus_plus/code_section.rb +0 -68
  55. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  56. data/lib/csv_plus_plus/language/cell_value.tab.rb +0 -332
  57. data/lib/csv_plus_plus/language/code_section.tab.rb +0 -442
  58. data/lib/csv_plus_plus/language/compiler.rb +0 -157
  59. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  60. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  61. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  62. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  63. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -26
  64. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  65. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  66. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  67. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  68. data/lib/csv_plus_plus/language/entities.rb +0 -28
  69. data/lib/csv_plus_plus/language/references.rb +0 -70
  70. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  71. data/lib/csv_plus_plus/language/scope.rb +0 -188
  72. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../entities'
4
-
5
- module CSVPlusPlus
6
- module Language
7
- module Entities
8
- # A basic building block of the abstract syntax tree (AST)
9
- #
10
- # @attr_reader id [Symbol] The identifier of the entity. For functions this is the function name,
11
- # for variables it's the variable name
12
- # @attr_reader type [Symbol] The type of the entity. Valid values are defined in +::CSVPlusPlus::Language::Types+
13
- class Entity
14
- attr_reader :id, :type
15
-
16
- # @param type [::String, Symbol]
17
- # @param id [::String, nil]
18
- def initialize(type, id: nil)
19
- @type = type.to_sym
20
- @id = id.downcase.to_sym if id
21
- end
22
-
23
- # @return [boolean]
24
- def ==(other)
25
- self.class == other.class && @type == other.type && @id == other.id
26
- end
27
-
28
- # Respond to predicates that correspond to types like #boolean?, #string?, etc
29
- #
30
- # @param method_name [Symbol] The +method_name+ to respond to
31
- def method_missing(method_name, *_arguments)
32
- if method_name =~ /^(\w+)\?$/
33
- t = ::Regexp.last_match(1)
34
- a_type?(t) && @type == t.to_sym
35
- else
36
- super
37
- end
38
- end
39
-
40
- # Respond to predicates by type (entity.boolean?, entity.string?, etc)
41
- #
42
- # @param method_name [Symbol] The +method_name+ to respond to
43
- #
44
- # @return [boolean]
45
- def respond_to_missing?(method_name, *_arguments)
46
- (method_name =~ /^(\w+)\?$/ && a_type?(::Regexp.last_match(1))) || super
47
- end
48
-
49
- private
50
-
51
- def a_type?(str)
52
- ::CSVPlusPlus::Language::TYPES.include?(str.to_sym)
53
- end
54
- end
55
-
56
- # An entity that can take other entities as arguments. Current use cases for this
57
- # are function calls and function definitions
58
- #
59
- # @attr_reader arguments [Array<Entity>] The arguments supplied to this entity.
60
- class EntityWithArguments < Entity
61
- attr_reader :arguments
62
-
63
- # @param type [::String, Symbol]
64
- # @param id [::String]
65
- # @param arguments [Array<Entity>]
66
- def initialize(type, id: nil, arguments: [])
67
- super(type, id:)
68
- @arguments = arguments
69
- end
70
-
71
- # @return [boolean]
72
- def ==(other)
73
- super && @arguments == other.arguments
74
- end
75
-
76
- protected
77
-
78
- attr_writer :arguments
79
-
80
- def arguments_to_s
81
- @arguments.join(', ')
82
- end
83
- end
84
- end
85
- end
86
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative './entity'
4
-
5
- module CSVPlusPlus
6
- module Language
7
- module Entities
8
- # A function definition
9
- #
10
- # @attr_reader body [Entity] The body of the function. +body+ can contain variable references
11
- # from +@arguments+
12
- class Function < EntityWithArguments
13
- attr_reader :body
14
-
15
- # @param id [Symbool, String] the name of the function - what it will be callable by
16
- # @param arguments [Array<Symbol>]
17
- # @param body [Entity]
18
- def initialize(id, arguments, body)
19
- super(:function, id:, arguments: arguments.map(&:to_sym))
20
- @body = body
21
- end
22
-
23
- # @return [String]
24
- def to_s
25
- "def #{@id.to_s.upcase}(#{arguments_to_s}) #{@body}"
26
- end
27
-
28
- # @return [boolean]
29
- def ==(other)
30
- super && @body == other.body
31
- end
32
- end
33
- end
34
- end
35
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Language
5
- module Entities
6
- # A function call
7
- class FunctionCall < EntityWithArguments
8
- # @param id [String] The name of the function
9
- # @param arguments [Array<Entity>] The arguments to the function
10
- def initialize(id, arguments)
11
- super(:function_call, id:, arguments:)
12
- end
13
-
14
- # @return [String]
15
- def to_s
16
- "#{@id.to_s.upcase}(#{arguments_to_s})"
17
- end
18
-
19
- # @return [boolean]
20
- def ==(other)
21
- super && @id == other.id
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Language
5
- module Entities
6
- # A number value
7
- #
8
- # @attr_reader value [Numeric] The parsed number value
9
- class Number < Entity
10
- attr_reader :value
11
-
12
- # @param value [String, Numeric] Either a +String+ that looks like a number, or an already parsed Numeric
13
- def initialize(value)
14
- super(:number)
15
-
16
- @value =
17
- if value.instance_of?(::String)
18
- value.include?('.') ? Float(value) : Integer(value, 10)
19
- else
20
- value
21
- end
22
- end
23
-
24
- # @return [String]
25
- def to_s
26
- @value.to_s
27
- end
28
-
29
- # @return [boolean]
30
- def ==(other)
31
- super && value == other.value
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Language
5
- module Entities
6
- # A runtime value. These are values which can be materialized at any point via the +resolve_fn+
7
- # which takes an ExecutionContext as a param
8
- #
9
- # @attr_reader resolve_fn [lambda] A lambda that is called when the runtime value is resolved
10
- class RuntimeValue < Entity
11
- attr_reader :arguments, :resolve_fn
12
-
13
- # @param resolve_fn [lambda] A lambda that is called when the runtime value is resolved
14
- def initialize(resolve_fn, arguments: nil)
15
- super(:runtime_value)
16
-
17
- @arguments = arguments
18
- @resolve_fn = resolve_fn
19
- end
20
-
21
- # @return [String]
22
- def to_s
23
- '(runtime_value)'
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Language
5
- module Entities
6
- # A string value
7
- #
8
- # @attr_reader value [String]
9
- class String < Entity
10
- attr_reader :value
11
-
12
- # @param value [String] The string that has been parsed out of the template
13
- def initialize(value)
14
- super(:string)
15
-
16
- @value = value.gsub(/^"|"$/, '')
17
- end
18
-
19
- # @return [String]
20
- def to_s
21
- "\"#{@value}\""
22
- end
23
-
24
- # @return [boolean]
25
- def ==(other)
26
- super && value == other.value
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CSVPlusPlus
4
- module Language
5
- module Entities
6
- # A reference to a variable
7
- class Variable < Entity
8
- # initialize
9
- def initialize(id)
10
- super(:variable, id:)
11
- end
12
-
13
- # to_s
14
- def to_s
15
- "$$#{@id}"
16
- end
17
-
18
- # ==
19
- def ==(other)
20
- super && id == other.id
21
- end
22
- end
23
- end
24
- end
25
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'entities/boolean'
4
- require_relative 'entities/cell_reference'
5
- require_relative 'entities/entity'
6
- require_relative 'entities/function'
7
- require_relative 'entities/function_call'
8
- require_relative 'entities/number'
9
- require_relative 'entities/runtime_value'
10
- require_relative 'entities/string'
11
- require_relative 'entities/variable'
12
-
13
- module CSVPlusPlus
14
- module Language
15
- TYPES = {
16
- boolean: ::CSVPlusPlus::Language::Entities::Boolean,
17
- cell_reference: ::CSVPlusPlus::Language::Entities::CellReference,
18
- function: ::CSVPlusPlus::Language::Entities::Function,
19
- function_call: ::CSVPlusPlus::Language::Entities::FunctionCall,
20
- number: ::CSVPlusPlus::Language::Entities::Number,
21
- runtime_value: ::CSVPlusPlus::Language::Entities::RuntimeValue,
22
- string: ::CSVPlusPlus::Language::Entities::String,
23
- variable: ::CSVPlusPlus::Language::Entities::Variable
24
- }.freeze
25
-
26
- public_constant :TYPES
27
- end
28
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../graph'
4
- require_relative './scope'
5
-
6
- module CSVPlusPlus
7
- module Language
8
- # References in an AST that need to be resolved
9
- #
10
- # @attr functions [Array<Entities::Function>] Functions references
11
- # @attr variables [Array<Entities::Variable>] Variable references
12
- class References
13
- attr_accessor :functions, :variables
14
-
15
- # Extract references from an AST and return them in a new +References+ object
16
- #
17
- # @param ast [Entity] An +Entity+ to do a depth first search on for references. Entities can be
18
- # infinitely deep because they can contain other function calls as params to a function call
19
- # @param code_section [CodeSection] The +CodeSection+ containing all currently defined functions
20
- #
21
- # @return [References]
22
- def self.extract(ast, code_section)
23
- new.tap do |refs|
24
- ::CSVPlusPlus::Graph.depth_first_search(ast) do |node|
25
- next unless node.function_call? || node.variable?
26
-
27
- refs.functions << node if function_reference?(node, code_section)
28
- refs.variables << node if node.variable?
29
- end
30
- end
31
- end
32
-
33
- # Is the node a resolvable reference?
34
- #
35
- # @param node [Entity] The node to check if it's resolvable
36
- #
37
- # @return [boolean]
38
- # TODO: move this into the Entity subclasses
39
- def self.function_reference?(node, code_section)
40
- node.function_call? && (code_section.defined_function?(node.id) \
41
- || ::CSVPlusPlus::Language::Builtins::FUNCTIONS.key?(node.id))
42
- end
43
-
44
- private_class_method :function_reference?
45
-
46
- # Create an object with empty references. The caller will build them up as it depth-first-searches
47
- def initialize
48
- @functions = []
49
- @variables = []
50
- end
51
-
52
- # Are there any references to be resolved?
53
- #
54
- # @return [boolean]
55
- def empty?
56
- @functions.empty? && @variables.empty?
57
- end
58
-
59
- # @return [String]
60
- def to_s
61
- "References(functions: #{@functions}, variables: #{@variables})"
62
- end
63
-
64
- # @return [boolean]
65
- def ==(other)
66
- @functions == other.functions && @variables == other.variables
67
- end
68
- end
69
- end
70
- end
@@ -1,205 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'entities'
4
- require_relative 'syntax_error'
5
- require 'tempfile'
6
-
7
- module CSVPlusPlus
8
- module Language
9
- # The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc). We take
10
- # multiple runs through the input file for parsing so it's really convenient to have a central place for these
11
- # things to be managed.
12
- #
13
- # @attr_reader filename [String, nil] The filename that the input came from (mostly used for debugging since
14
- # +filename+ can be +nil+ if it's read from stdin.
15
- # @attr_reader length_of_code_section [Integer] The length (count of lines) of the code section part of the original
16
- # input.
17
- # @attr_reader length_of_csv_section [Integer] The length (count of lines) of the CSV part of the original csvpp
18
- # input.
19
- # @attr_reader length_of_original_file [Integer] The length (count of lines) of the original csvpp input.
20
- #
21
- # @attr cell [Cell] The current cell being processed
22
- # @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
23
- # @attr row_index [Integer] The index of the current row being processed (starts at 0)
24
- # @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
25
- class Runtime
26
- attr_reader :filename, :length_of_code_section, :length_of_csv_section, :length_of_original_file
27
-
28
- attr_accessor :cell, :cell_index, :row_index, :line_number
29
-
30
- # @param input [String] The input to be parsed
31
- # @param filename [String, nil] The filename that the input came from (mostly used for debugging since +filename+
32
- # can be +nil+ if it's read from stdin
33
- def initialize(input:, filename:)
34
- @filename = filename || 'stdin'
35
-
36
- init_input!(input)
37
- start!
38
- end
39
-
40
- # Map over an a csvpp file and keep track of line_number and row_index
41
- #
42
- # @param lines [Array]
43
- #
44
- # @return [Array]
45
- def map_lines(lines, &block)
46
- @line_number = 1
47
- lines.map do |line|
48
- block.call(line).tap { next_line! }
49
- end
50
- end
51
-
52
- # Map over a single row and keep track of the cell and it's index
53
- #
54
- # @param row [Array<Cell>] The row to map each cell over
55
- #
56
- # @return [Array]
57
- def map_row(row, &block)
58
- @cell_index = 0
59
- row.map.with_index do |cell, index|
60
- set_cell!(cell, index)
61
- block.call(cell, index)
62
- end
63
- end
64
-
65
- # Map over all rows and keep track of row and line numbers
66
- #
67
- # @param rows [Array<Row>] The rows to map over (and keep track of indexes)
68
- # @param cells_too [boolean] If the cells of each +row+ should be iterated over also.
69
- #
70
- # @return [Array]
71
- def map_rows(rows, cells_too: false, &block)
72
- @row_index = 0
73
- map_lines(rows) do |row|
74
- if cells_too
75
- # it's either CSV or a Row object
76
- map_row((row.is_a?(::CSVPlusPlus::Row) ? row.cells : row), &block)
77
- else
78
- block.call(row)
79
- end
80
- end
81
- end
82
-
83
- # Increment state to the next line
84
- #
85
- # @return [Integer]
86
- def next_line!
87
- @row_index += 1 unless @row_index.nil?
88
- @line_number += 1
89
- end
90
-
91
- # Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
92
- #
93
- # @return [Integer, nil]
94
- def rownum
95
- return if @row_index.nil?
96
-
97
- @row_index + 1
98
- end
99
-
100
- # Set the current cell and index
101
- #
102
- # @param cell [Cell] The current cell
103
- # @param cell_index [Integer] The index of the cell
104
- def set_cell!(cell, cell_index)
105
- @cell = cell
106
- @cell_index = cell_index
107
- end
108
-
109
- # Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
110
- def start!
111
- @row_index = @cell_index = nil
112
- @line_number = 1
113
- end
114
-
115
- # Reset the runtime state starting at the CSV section
116
- def start_at_csv!
117
- # TODO: isn't the input re-written anyway without the code section? why do we need this?
118
- start!
119
- @line_number = @length_of_code_section || 1
120
- end
121
-
122
- # @return [String]
123
- def to_s
124
- "Runtime(cell: #{@cell}, row_index: #{@row_index}, cell_index: #{@cell_index})"
125
- end
126
-
127
- # Get the current (entity) value of a runtime value
128
- #
129
- # @param var_id [String, Symbol] The Variable#id of the variable being resolved.
130
- #
131
- # @return [Entity]
132
- def runtime_value(var_id)
133
- if runtime_variable?(var_id)
134
- ::CSVPlusPlus::Language::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
135
- else
136
- raise_syntax_error('Undefined variable', var_id)
137
- end
138
- end
139
-
140
- # Is +var_id+ a runtime variable? (it's a static variable otherwise)
141
- #
142
- # @param var_id [String, Symbol] The Variable#id to check if it's a runtime variable
143
- #
144
- # @return [boolean]
145
- def runtime_variable?(var_id)
146
- ::CSVPlusPlus::Language::Builtins::VARIABLES.key?(var_id.to_sym)
147
- end
148
-
149
- # Called when an error is encoutered during parsing. It will construct a useful
150
- # error with the current +@row/@cell_index+, +@line_number+ and +@filename+
151
- #
152
- # @param message [String] A message relevant to why this error is being raised.
153
- # @param bad_input [String] The offending input that caused this error to be thrown.
154
- # @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
155
- def raise_syntax_error(message, bad_input, wrapped_error: nil)
156
- raise(::CSVPlusPlus::Language::SyntaxError.new(message, bad_input, self, wrapped_error:))
157
- end
158
-
159
- # The currently available input for parsing. The tmp state will be re-written
160
- # between parsing the code section and the CSV section
161
- #
162
- # @return [String]
163
- def input
164
- @tmp
165
- end
166
-
167
- # We mutate the input over and over. It's ok because it's just a Tempfile
168
- #
169
- # @param data [String] The data to rewrite our input file to
170
- def rewrite_input!(data)
171
- @tmp.truncate(0)
172
- @tmp.write(data)
173
- @tmp.rewind
174
- end
175
-
176
- # Clean up the Tempfile we're using for parsing
177
- def cleanup!
178
- return unless @tmp
179
-
180
- @tmp.close
181
- @tmp.unlink
182
- @tmp = nil
183
- end
184
-
185
- private
186
-
187
- def count_code_section_lines(lines)
188
- eoc = ::CSVPlusPlus::Lexer::END_OF_CODE_SECTION
189
- lines.include?(eoc) ? (lines.take_while { |l| l != eoc }).length + 1 : 0
190
- end
191
-
192
- def init_input!(input)
193
- lines = (input || '').split(/\s*\n\s*/)
194
- @length_of_original_file = lines.length
195
- @length_of_code_section = count_code_section_lines(lines)
196
- @length_of_csv_section = @length_of_original_file - @length_of_code_section
197
-
198
- # we're gonna take our input file, write it to a tmp file then each
199
- # step is gonna mutate that tmp file
200
- @tmp = ::Tempfile.new
201
- rewrite_input!(input)
202
- end
203
- end
204
- end
205
- end