csv_plus_plus 0.1.0 → 0.1.2

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 (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