csv_plus_plus 0.0.5 → 0.1.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +1 -0
  4. data/lib/csv_plus_plus/cell.rb +24 -8
  5. data/lib/csv_plus_plus/cli.rb +29 -16
  6. data/lib/csv_plus_plus/cli_flag.rb +10 -2
  7. data/lib/csv_plus_plus/code_section.rb +55 -3
  8. data/lib/csv_plus_plus/color.rb +19 -5
  9. data/lib/csv_plus_plus/google_options.rb +6 -2
  10. data/lib/csv_plus_plus/graph.rb +0 -1
  11. data/lib/csv_plus_plus/language/ast_builder.rb +68 -0
  12. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +65 -0
  13. data/lib/csv_plus_plus/language/builtins.rb +46 -0
  14. data/lib/csv_plus_plus/language/cell_value.tab.rb +106 -134
  15. data/lib/csv_plus_plus/language/code_section.tab.rb +163 -192
  16. data/lib/csv_plus_plus/language/compiler.rb +75 -92
  17. data/lib/csv_plus_plus/language/entities/boolean.rb +3 -2
  18. data/lib/csv_plus_plus/language/entities/cell_reference.rb +10 -3
  19. data/lib/csv_plus_plus/language/entities/entity.rb +20 -8
  20. data/lib/csv_plus_plus/language/entities/function.rb +6 -4
  21. data/lib/csv_plus_plus/language/entities/function_call.rb +17 -5
  22. data/lib/csv_plus_plus/language/entities/number.rb +6 -4
  23. data/lib/csv_plus_plus/language/entities/runtime_value.rb +9 -8
  24. data/lib/csv_plus_plus/language/entities/string.rb +6 -4
  25. data/lib/csv_plus_plus/language/references.rb +22 -5
  26. data/lib/csv_plus_plus/language/runtime.rb +80 -22
  27. data/lib/csv_plus_plus/language/scope.rb +34 -39
  28. data/lib/csv_plus_plus/language/syntax_error.rb +10 -5
  29. data/lib/csv_plus_plus/lexer/lexer.rb +27 -13
  30. data/lib/csv_plus_plus/lexer/tokenizer.rb +35 -11
  31. data/lib/csv_plus_plus/modifier.rb +38 -18
  32. data/lib/csv_plus_plus/modifier.tab.rb +2 -2
  33. data/lib/csv_plus_plus/options.rb +20 -2
  34. data/lib/csv_plus_plus/row.rb +15 -4
  35. data/lib/csv_plus_plus/template.rb +26 -6
  36. data/lib/csv_plus_plus/version.rb +1 -1
  37. data/lib/csv_plus_plus/writer/excel.rb +2 -9
  38. data/lib/csv_plus_plus/writer/file_backer_upper.rb +22 -20
  39. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +8 -10
  40. data/lib/csv_plus_plus/writer/google_sheets.rb +4 -10
  41. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +23 -15
  42. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +15 -8
  43. data/lib/csv_plus_plus.rb +42 -8
  44. metadata +5 -2
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'benchmark'
4
3
  require 'csv'
5
- require_relative '../cell'
6
- require_relative '../modifier'
7
- require_relative '../modifier.tab'
8
- require_relative '../row'
9
- require_relative '../template'
4
+
5
+ require_relative 'benchmarked_compiler'
10
6
  require_relative 'code_section.tab'
11
7
  require_relative 'entities'
12
8
  require_relative 'runtime'
@@ -14,61 +10,75 @@ require_relative 'scope'
14
10
 
15
11
  module CSVPlusPlus
16
12
  module Language
17
- # Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+).
18
- # Variable resolution is delegated to the +Scope+
19
- # rubocop:disable Metrics/ClassLength
13
+ # Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+). Variable resolution is delegated
14
+ # to the +Scope+
15
+ #
16
+ # @attr_reader options [Options] The +Options+ to compile with
17
+ # @attr_reader runtime [Runtime] The runtime execution
18
+ # @attr_reader scope [Scope] +Scope+ for variable resolution
20
19
  class Compiler
21
20
  attr_reader :timings, :benchmark, :options, :runtime, :scope
22
21
 
23
22
  # Create a compiler and make sure it gets cleaned up
24
- def self.with_compiler(input:, filename:, options:, &block)
25
- runtime = ::CSVPlusPlus::Language::Runtime.new(filename:, input:)
26
-
23
+ #
24
+ # @param runtime [Runtime] The initial +Runtime+ for the compiler
25
+ # @param options [Options]
26
+ def self.with_compiler(runtime:, options:, &block)
27
+ compiler = new(options:, runtime:)
27
28
  if options.verbose
28
- compiler_with_timings(runtime:, options:) do |c|
29
+ ::CSVPlusPlus::Language::BenchmarkedCompiler.with_benchmarks(compiler) do |c|
29
30
  block.call(c)
30
31
  end
31
32
  else
32
- yield(new(runtime:, options:))
33
+ yield(compiler)
33
34
  end
34
35
  ensure
35
36
  runtime.cleanup!
36
37
  end
37
38
 
38
- # Create a compiler that can time each of it's stages
39
- def self.compiler_with_timings(options:, runtime:, &block)
40
- ::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
41
- compiler = new(options:, runtime:, benchmark: x)
42
- block.call(compiler)
43
- [compiler.timings.reduce(:+)]
44
- end
45
- end
46
-
47
- # initialize
48
- def initialize(runtime:, options:, scope: nil, benchmark: nil)
39
+ # @param runtime [Runtime]
40
+ # @param options [Options]
41
+ # @param scope [Scope, nil]
42
+ def initialize(runtime:, options:, scope: nil)
49
43
  @options = options
50
44
  @runtime = runtime
51
45
  @scope = scope || ::CSVPlusPlus::Language::Scope.new(runtime:)
52
- @benchmark = benchmark
53
- @timings = [] if benchmark
54
46
  end
55
47
 
56
- # Parse an entire template and return a +::CSVPlusPlus::Template+ instance
57
- def parse_template
48
+ # Write the compiled results
49
+ def outputting!
50
+ @runtime.start_at_csv!
51
+ yield
52
+ end
53
+
54
+ # Compile a template and return a +::CSVPlusPlus::Template+ instance ready to be written with a +Writer+
55
+ #
56
+ # @return [Template]
57
+ def compile_template
58
58
  parse_code_section!
59
59
  rows = parse_csv_section!
60
60
 
61
- ::CSVPlusPlus::Template.new(rows:, scope: @scope).tap do |t|
61
+ ::CSVPlusPlus::Template.new(rows:, code_section: scope.code_section).tap do |t|
62
62
  t.validate_infinite_expands(@runtime)
63
63
  expanding { t.expand_rows! }
64
64
  resolve_all_cells!(t)
65
65
  end
66
66
  end
67
67
 
68
- # parses the input file and returns a +CodeSection+
68
+ # @return [String]
69
+ def to_s
70
+ "Compiler(options: #{@options}, runtime: #{@runtime}, scope: #{@scope})"
71
+ end
72
+
73
+ protected
74
+
75
+ # Parses the input file and returns a +CodeSection+
76
+ #
77
+ # @return [CodeSection]
69
78
  def parse_code_section!
79
+ @runtime.start!
70
80
  parsing_code_section do |input|
71
- code_section, csv_section = ::CSVPlusPlus::Language::CodeSectionParser.new.parse(input, self)
81
+ code_section, csv_section = ::CSVPlusPlus::Language::CodeSectionParser.new.parse(input, @runtime)
72
82
  # TODO: infer a type
73
83
  # allow user-supplied key/values to override anything global or from the code section
74
84
  code_section.def_variables(
@@ -82,20 +92,48 @@ module CSVPlusPlus
82
92
  @scope.code_section
83
93
  end
84
94
 
85
- # workflow when parsing csv
95
+ # Parse the CSV section and return an array of +Row+s
96
+ #
97
+ # @return [Array<Row>]
86
98
  def parse_csv_section!
87
- workflow(stage: 'Parsing CSV section') do
88
- @runtime.map_rows(::CSV.new(runtime.input)) do |csv_row|
89
- parse_row(csv_row)
90
- end
99
+ @runtime.start_at_csv!
100
+ @runtime.map_rows(::CSV.new(runtime.input)) do |csv_row|
101
+ parse_row(csv_row)
91
102
  end
92
103
  ensure
93
104
  # we're done with the file and everything is in memory
94
105
  @runtime.cleanup!
95
106
  end
96
107
 
108
+ # Iterates through each cell of each row and resolves it's variable and function references.
109
+ #
110
+ # @param template [Template]
111
+ # @return [Array<Entity>]
112
+ def resolve_all_cells!(template)
113
+ @runtime.start_at_csv!
114
+ @runtime.map_rows(template.rows, cells_too: true) do |cell|
115
+ cell.ast = @scope.resolve_cell_value if cell.ast
116
+ end
117
+ end
118
+
119
+ # Expanding rows
120
+ def expanding
121
+ @runtime.start_at_csv!
122
+ yield
123
+ end
124
+
125
+ private
126
+
127
+ def parsing_code_section
128
+ csv_section = yield(@runtime.input.read)
129
+ @runtime.rewrite_input!(csv_section)
130
+ end
131
+
97
132
  # Using the current +@runtime+ and the given +csv_row+ parse it into a +Row+ of +Cell+s
98
133
  # +csv_row+ should have already been run through a CSV parser and is an array of strings
134
+ #
135
+ # @param csv_row [Array<Array<String>>]
136
+ # @return [Row]
99
137
  def parse_row(csv_row)
100
138
  row_modifier = ::CSVPlusPlus::Modifier.new(row_level: true)
101
139
 
@@ -109,61 +147,6 @@ module CSVPlusPlus
109
147
 
110
148
  ::CSVPlusPlus::Row.new(@runtime.row_index, cells, row_modifier)
111
149
  end
112
-
113
- # workflow when resolving the values of all cells
114
- def resolve_all_cells!(template)
115
- workflow(stage: 'Resolving each cell') do
116
- @runtime.map_rows(template.rows, cells_too: true) do |cell|
117
- cell.ast = @scope.resolve_cell_value if cell.ast
118
- end
119
- end
120
- end
121
-
122
- # workflow when writing results
123
- def outputting!(&block)
124
- workflow(stage: 'Writing the spreadsheet') do
125
- block.call
126
- end
127
- end
128
-
129
- # to_s
130
- def to_s
131
- "Compiler(options: #{@options}, runtime: #{@runtime}, scope: #{@scope})"
132
- end
133
-
134
- private
135
-
136
- # workflow when parsing the code section
137
- def parsing_code_section(&block)
138
- workflow(
139
- stage: 'Parsing code section',
140
- processing_code_section: true
141
- ) do
142
- csv_section = block.call(@runtime.input.read)
143
- @runtime.rewrite_input!(csv_section)
144
- end
145
- end
146
-
147
- # workflow when expanding rows
148
- def expanding(&block)
149
- workflow(stage: 'Expanding rows') do
150
- block.call
151
- end
152
- end
153
-
154
- def workflow(stage:, processing_code_section: false, &block)
155
- @runtime.init!(processing_code_section ? 1 : (@runtime.length_of_code_section || 1))
156
-
157
- ret = nil
158
- if @benchmark
159
- @timings << @benchmark.report(stage) { ret = block.call }
160
- else
161
- ret = block.call
162
- end
163
-
164
- ret
165
- end
166
150
  end
167
- # rubocop:enable Metrics/ClassLength
168
151
  end
169
152
  end
@@ -6,10 +6,11 @@ module CSVPlusPlus
6
6
  module Language
7
7
  module Entities
8
8
  # A boolean value
9
+ #
10
+ # @attr_reader value [true, false]
9
11
  class Boolean < Entity
10
12
  attr_reader :value
11
13
 
12
- # initialize
13
14
  # @param value [String, Boolean]
14
15
  def initialize(value)
15
16
  super(:boolean)
@@ -22,7 +23,7 @@ module CSVPlusPlus
22
23
  @value.to_s.upcase
23
24
  end
24
25
 
25
- # @return [Boolean]
26
+ # @return [boolean]
26
27
  def ==(other)
27
28
  super && value == other.value
28
29
  end
@@ -5,21 +5,28 @@ require_relative './entity'
5
5
  module CSVPlusPlus
6
6
  module Language
7
7
  module Entities
8
- ##
9
8
  # A reference to a cell
9
+ #
10
+ # @attr_reader cell_reference [String] The cell reference in A1 format
10
11
  class CellReference < Entity
11
12
  attr_reader :cell_reference
12
13
 
13
- # initialize
14
+ # @param cell_reference [String] The cell reference in A1 format
14
15
  def initialize(cell_reference)
15
16
  super(:cell_reference)
17
+
16
18
  @cell_reference = cell_reference
17
19
  end
18
20
 
19
- # to_s
21
+ # @return [String]
20
22
  def to_s
21
23
  @cell_reference
22
24
  end
25
+
26
+ # @return [Boolean]
27
+ def ==(other)
28
+ super && @cell_reference == other.cell_reference
29
+ end
23
30
  end
24
31
  end
25
32
  end
@@ -6,22 +6,28 @@ module CSVPlusPlus
6
6
  module Language
7
7
  module Entities
8
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+
9
13
  class Entity
10
14
  attr_reader :id, :type
11
15
 
12
- # @param type [String, Symbol]
13
- # @param id [String]
16
+ # @param type [::String, Symbol]
17
+ # @param id [::String, nil]
14
18
  def initialize(type, id: nil)
15
19
  @type = type.to_sym
16
20
  @id = id.downcase.to_sym if id
17
21
  end
18
22
 
19
- # @return [Boolean]
23
+ # @return [boolean]
20
24
  def ==(other)
21
25
  self.class == other.class && @type == other.type && @id == other.id
22
26
  end
23
27
 
24
28
  # Respond to predicates that correspond to types like #boolean?, #string?, etc
29
+ #
30
+ # @param method_name [Symbol] The +method_name+ to respond to
25
31
  def method_missing(method_name, *_arguments)
26
32
  if method_name =~ /^(\w+)\?$/
27
33
  t = ::Regexp.last_match(1)
@@ -32,7 +38,10 @@ module CSVPlusPlus
32
38
  end
33
39
 
34
40
  # Respond to predicates by type (entity.boolean?, entity.string?, etc)
35
- # @return [Boolean]
41
+ #
42
+ # @param method_name [Symbol] The +method_name+ to respond to
43
+ #
44
+ # @return [boolean]
36
45
  def respond_to_missing?(method_name, *_arguments)
37
46
  (method_name =~ /^(\w+)\?$/ && a_type?(::Regexp.last_match(1))) || super
38
47
  end
@@ -44,19 +53,22 @@ module CSVPlusPlus
44
53
  end
45
54
  end
46
55
 
47
- # An entity that can take other entities as arguments
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.
48
60
  class EntityWithArguments < Entity
49
61
  attr_reader :arguments
50
62
 
51
- # @param type [String, Symbol]
52
- # @param id [String]
63
+ # @param type [::String, Symbol]
64
+ # @param id [::String]
53
65
  # @param arguments [Array<Entity>]
54
66
  def initialize(type, id: nil, arguments: [])
55
67
  super(type, id:)
56
68
  @arguments = arguments
57
69
  end
58
70
 
59
- # @return [Boolean]
71
+ # @return [boolean]
60
72
  def ==(other)
61
73
  super && @arguments == other.arguments
62
74
  end
@@ -6,24 +6,26 @@ module CSVPlusPlus
6
6
  module Language
7
7
  module Entities
8
8
  # A function definition
9
+ #
10
+ # @attr_reader body [Entity] The body of the function. +body+ can contain variable references
11
+ # from +@arguments+
9
12
  class Function < EntityWithArguments
10
13
  attr_reader :body
11
14
 
12
- # Create a function
13
15
  # @param id [Symbool, String] the name of the function - what it will be callable by
14
- # @param arguments [Array(Symbol)]
16
+ # @param arguments [Array<Symbol>]
15
17
  # @param body [Entity]
16
18
  def initialize(id, arguments, body)
17
19
  super(:function, id:, arguments: arguments.map(&:to_sym))
18
20
  @body = body
19
21
  end
20
22
 
21
- # to_s
23
+ # @return [String]
22
24
  def to_s
23
25
  "def #{@id.to_s.upcase}(#{arguments_to_s}) #{@body}"
24
26
  end
25
27
 
26
- # ==
28
+ # @return [boolean]
27
29
  def ==(other)
28
30
  super && @body == other.body
29
31
  end
@@ -4,18 +4,30 @@ module CSVPlusPlus
4
4
  module Language
5
5
  module Entities
6
6
  # A function call
7
+ #
8
+ # @attr_reader infix [boolean] Whether or not this function call is infix (X * Y, A + B, etc)
7
9
  class FunctionCall < EntityWithArguments
8
- # initialize
9
- def initialize(id, arguments)
10
+ attr_reader :infix
11
+
12
+ # @param id [String] The name of the function
13
+ # @param arguments [Array<Entity>] The arguments to the function
14
+ # @param infix [boolean] Whether the function is infix
15
+ def initialize(id, arguments, infix: false)
10
16
  super(:function_call, id:, arguments:)
17
+
18
+ @infix = infix
11
19
  end
12
20
 
13
- # to_s
21
+ # @return [String]
14
22
  def to_s
15
- "#{@id.to_s.upcase}(#{arguments_to_s})"
23
+ if @infix
24
+ "(#{arguments.join(" #{@id} ")})"
25
+ else
26
+ "#{@id.to_s.upcase}(#{arguments_to_s})"
27
+ end
16
28
  end
17
29
 
18
- # ==
30
+ # @return [boolean]
19
31
  def ==(other)
20
32
  super && @id == other.id
21
33
  end
@@ -3,14 +3,16 @@
3
3
  module CSVPlusPlus
4
4
  module Language
5
5
  module Entities
6
- ##
7
6
  # A number value
7
+ #
8
+ # @attr_reader value [Numeric] The parsed number value
8
9
  class Number < Entity
9
10
  attr_reader :value
10
11
 
11
- # initialize
12
+ # @param value [String, Numeric] Either a +String+ that looks like a number, or an already parsed Numeric
12
13
  def initialize(value)
13
14
  super(:number)
15
+
14
16
  @value =
15
17
  if value.instance_of?(::String)
16
18
  value.include?('.') ? Float(value) : Integer(value, 10)
@@ -19,12 +21,12 @@ module CSVPlusPlus
19
21
  end
20
22
  end
21
23
 
22
- # to_s
24
+ # @return [String]
23
25
  def to_s
24
26
  @value.to_s
25
27
  end
26
28
 
27
- # ==
29
+ # @return [boolean]
28
30
  def ==(other)
29
31
  super && value == other.value
30
32
  end
@@ -3,21 +3,22 @@
3
3
  module CSVPlusPlus
4
4
  module Language
5
5
  module Entities
6
- ##
7
- # A runtime value
8
- #
9
- # These are values which can be materialized at any point via the +resolve_fn+
6
+ # A runtime value. These are values which can be materialized at any point via the +resolve_fn+
10
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
11
10
  class RuntimeValue < Entity
12
- attr_reader :resolve_fn
11
+ attr_reader :arguments, :resolve_fn
13
12
 
14
- # initialize
15
- def initialize(resolve_fn)
13
+ # @param resolve_fn [lambda] A lambda that is called when the runtime value is resolved
14
+ def initialize(resolve_fn, arguments: nil)
16
15
  super(:runtime_value)
16
+
17
+ @arguments = arguments
17
18
  @resolve_fn = resolve_fn
18
19
  end
19
20
 
20
- # to_s
21
+ # @return [String]
21
22
  def to_s
22
23
  '(runtime_value)'
23
24
  end
@@ -3,23 +3,25 @@
3
3
  module CSVPlusPlus
4
4
  module Language
5
5
  module Entities
6
- ##
7
6
  # A string value
7
+ #
8
+ # @attr_reader value [String]
8
9
  class String < Entity
9
10
  attr_reader :value
10
11
 
11
- # initialize
12
+ # @param value [String] The string that has been parsed out of the template
12
13
  def initialize(value)
13
14
  super(:string)
15
+
14
16
  @value = value.gsub(/^"|"$/, '')
15
17
  end
16
18
 
17
- # to_s
19
+ # @return [String]
18
20
  def to_s
19
21
  "\"#{@value}\""
20
22
  end
21
23
 
22
- # ==
24
+ # @return [boolean]
23
25
  def ==(other)
24
26
  super && value == other.value
25
27
  end
@@ -6,10 +6,19 @@ require_relative './scope'
6
6
  module CSVPlusPlus
7
7
  module Language
8
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
9
12
  class References
10
13
  attr_accessor :functions, :variables
11
14
 
12
- # Extract references from an AST. And return them in a new +References+ object
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]
13
22
  def self.extract(ast, code_section)
14
23
  new.tap do |refs|
15
24
  ::CSVPlusPlus::Graph.depth_first_search(ast) do |node|
@@ -22,8 +31,14 @@ module CSVPlusPlus
22
31
  end
23
32
 
24
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
25
39
  def self.function_reference?(node, code_section)
26
- node.function_call? && (code_section.defined_function?(node.id) || ::BUILTIN_FUNCTIONS.key?(node.id))
40
+ node.function_call? && (code_section.defined_function?(node.id) \
41
+ || ::CSVPlusPlus::Language::Builtins::FUNCTIONS.key?(node.id))
27
42
  end
28
43
 
29
44
  private_class_method :function_reference?
@@ -34,17 +49,19 @@ module CSVPlusPlus
34
49
  @variables = []
35
50
  end
36
51
 
37
- # are there any references to be resolved?
52
+ # Are there any references to be resolved?
53
+ #
54
+ # @return [boolean]
38
55
  def empty?
39
56
  @functions.empty? && @variables.empty?
40
57
  end
41
58
 
42
- # to_s
59
+ # @return [String]
43
60
  def to_s
44
61
  "References(functions: #{@functions}, variables: #{@variables})"
45
62
  end
46
63
 
47
- # ==
64
+ # @return [boolean]
48
65
  def ==(other)
49
66
  @functions == other.functions && @variables == other.variables
50
67
  end