csv_plus_plus 0.0.5 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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