csv_plus_plus 0.0.5 → 0.1.0

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 +7 -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 +22 -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 +1 -2
  15. data/lib/csv_plus_plus/language/code_section.tab.rb +1 -2
  16. data/lib/csv_plus_plus/language/compiler.rb +74 -86
  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 +4 -3
  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 +29 -38
  28. data/lib/csv_plus_plus/language/syntax_error.rb +10 -5
  29. data/lib/csv_plus_plus/lexer/lexer.rb +25 -12
  30. data/lib/csv_plus_plus/lexer/tokenizer.rb +35 -11
  31. data/lib/csv_plus_plus/modifier.rb +38 -13
  32. data/lib/csv_plus_plus/modifier.tab.rb +2 -2
  33. data/lib/csv_plus_plus/options.rb +17 -2
  34. data/lib/csv_plus_plus/row.rb +15 -4
  35. data/lib/csv_plus_plus/template.rb +10 -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 +21 -3
  44. metadata +5 -2
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'benchmark'
4
3
  require 'csv'
4
+ # TODO: move some of these out to csv_plus_plus.rb
5
5
  require_relative '../cell'
6
6
  require_relative '../modifier'
7
7
  require_relative '../modifier.tab'
8
8
  require_relative '../row'
9
9
  require_relative '../template'
10
+ require_relative 'benchmarked_compiler'
10
11
  require_relative 'code_section.tab'
11
12
  require_relative 'entities'
12
13
  require_relative 'runtime'
@@ -14,59 +15,73 @@ require_relative 'scope'
14
15
 
15
16
  module CSVPlusPlus
16
17
  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
18
+ # Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+). Variable resolution is delegated
19
+ # to the +Scope+
20
+ #
21
+ # @attr_reader options [Options] The +Options+ to compile with
22
+ # @attr_reader runtime [Runtime] The runtime execution
23
+ # @attr_reader scope [Scope] +Scope+ for variable resolution
20
24
  class Compiler
21
25
  attr_reader :timings, :benchmark, :options, :runtime, :scope
22
26
 
23
27
  # 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
-
28
+ #
29
+ # @param runtime [Runtime] The initial +Runtime+ for the compiler
30
+ # @param options [Options]
31
+ def self.with_compiler(runtime:, options:, &block)
32
+ compiler = new(options:, runtime:)
27
33
  if options.verbose
28
- compiler_with_timings(runtime:, options:) do |c|
34
+ ::CSVPlusPlus::Language::BenchmarkedCompiler.with_benchmarks(compiler) do |c|
29
35
  block.call(c)
30
36
  end
31
37
  else
32
- yield(new(runtime:, options:))
38
+ yield(compiler)
33
39
  end
34
40
  ensure
35
41
  runtime.cleanup!
36
42
  end
37
43
 
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)
44
+ # @param runtime [Runtime]
45
+ # @param options [Options]
46
+ # @param scope [Scope, nil]
47
+ def initialize(runtime:, options:, scope: nil)
49
48
  @options = options
50
49
  @runtime = runtime
51
50
  @scope = scope || ::CSVPlusPlus::Language::Scope.new(runtime:)
52
- @benchmark = benchmark
53
- @timings = [] if benchmark
54
51
  end
55
52
 
56
- # Parse an entire template and return a +::CSVPlusPlus::Template+ instance
57
- def parse_template
53
+ # Write the compiled results
54
+ def outputting!
55
+ @runtime.start_at_csv!
56
+ yield
57
+ end
58
+
59
+ # Compile a template and return a +::CSVPlusPlus::Template+ instance ready to be written with a +Writer+
60
+ #
61
+ # @return [Template]
62
+ def compile_template
58
63
  parse_code_section!
59
64
  rows = parse_csv_section!
60
65
 
61
- ::CSVPlusPlus::Template.new(rows:, scope: @scope).tap do |t|
66
+ ::CSVPlusPlus::Template.new(rows:).tap do |t|
62
67
  t.validate_infinite_expands(@runtime)
63
68
  expanding { t.expand_rows! }
64
69
  resolve_all_cells!(t)
65
70
  end
66
71
  end
67
72
 
68
- # parses the input file and returns a +CodeSection+
73
+ # @return [String]
74
+ def to_s
75
+ "Compiler(options: #{@options}, runtime: #{@runtime}, scope: #{@scope})"
76
+ end
77
+
78
+ protected
79
+
80
+ # Parses the input file and returns a +CodeSection+
81
+ #
82
+ # @return [CodeSection]
69
83
  def parse_code_section!
84
+ @runtime.start!
70
85
  parsing_code_section do |input|
71
86
  code_section, csv_section = ::CSVPlusPlus::Language::CodeSectionParser.new.parse(input, self)
72
87
  # TODO: infer a type
@@ -82,20 +97,48 @@ module CSVPlusPlus
82
97
  @scope.code_section
83
98
  end
84
99
 
85
- # workflow when parsing csv
100
+ # Parse the CSV section and return an array of +Row+s
101
+ #
102
+ # @return [Array<Row>]
86
103
  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
104
+ @runtime.start_at_csv!
105
+ @runtime.map_rows(::CSV.new(runtime.input)) do |csv_row|
106
+ parse_row(csv_row)
91
107
  end
92
108
  ensure
93
109
  # we're done with the file and everything is in memory
94
110
  @runtime.cleanup!
95
111
  end
96
112
 
113
+ # Iterates through each cell of each row and resolves it's variable and function references.
114
+ #
115
+ # @param template [Template]
116
+ # @return [Array<Entity>]
117
+ def resolve_all_cells!(template)
118
+ @runtime.start_at_csv!
119
+ @runtime.map_rows(template.rows, cells_too: true) do |cell|
120
+ cell.ast = @scope.resolve_cell_value if cell.ast
121
+ end
122
+ end
123
+
124
+ # Expanding rows
125
+ def expanding
126
+ @runtime.start_at_csv!
127
+ yield
128
+ end
129
+
130
+ private
131
+
132
+ def parsing_code_section
133
+ csv_section = yield(@runtime.input.read)
134
+ @runtime.rewrite_input!(csv_section)
135
+ end
136
+
97
137
  # Using the current +@runtime+ and the given +csv_row+ parse it into a +Row+ of +Cell+s
98
138
  # +csv_row+ should have already been run through a CSV parser and is an array of strings
139
+ #
140
+ # @param csv_row [Array<Array<String>>]
141
+ # @return [Row]
99
142
  def parse_row(csv_row)
100
143
  row_modifier = ::CSVPlusPlus::Modifier.new(row_level: true)
101
144
 
@@ -109,61 +152,6 @@ module CSVPlusPlus
109
152
 
110
153
  ::CSVPlusPlus::Row.new(@runtime.row_index, cells, row_modifier)
111
154
  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
155
  end
167
- # rubocop:enable Metrics/ClassLength
168
156
  end
169
157
  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
@@ -5,17 +5,18 @@ module CSVPlusPlus
5
5
  module Entities
6
6
  # A function call
7
7
  class FunctionCall < EntityWithArguments
8
- # initialize
8
+ # @param id [String] The name of the function
9
+ # @param arguments [Array<Entity>] The arguments to the function
9
10
  def initialize(id, arguments)
10
11
  super(:function_call, id:, arguments:)
11
12
  end
12
13
 
13
- # to_s
14
+ # @return [String]
14
15
  def to_s
15
16
  "#{@id.to_s.upcase}(#{arguments_to_s})"
16
17
  end
17
18
 
18
- # ==
19
+ # @return [boolean]
19
20
  def ==(other)
20
21
  super && @id == other.id
21
22
  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