csv_plus_plus 0.1.2 → 0.2.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -5
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  6. data/lib/csv_plus_plus/cell.rb +29 -41
  7. data/lib/csv_plus_plus/cli.rb +53 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +71 -71
  9. data/lib/csv_plus_plus/color.rb +32 -7
  10. data/lib/csv_plus_plus/compiler.rb +98 -66
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
  12. data/lib/csv_plus_plus/entities/boolean.rb +26 -10
  13. data/lib/csv_plus_plus/entities/builtins.rb +66 -24
  14. data/lib/csv_plus_plus/entities/date.rb +42 -6
  15. data/lib/csv_plus_plus/entities/entity.rb +17 -69
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
  17. data/lib/csv_plus_plus/entities/function.rb +34 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +49 -10
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +30 -11
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
  23. data/lib/csv_plus_plus/entities/string.rb +23 -7
  24. data/lib/csv_plus_plus/entities.rb +7 -16
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +25 -2
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  33. data/lib/csv_plus_plus/error.rb +5 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +25 -10
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
  38. data/lib/csv_plus_plus/lexer.rb +64 -1
  39. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  40. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  41. data/lib/csv_plus_plus/modifier/expand.rb +78 -0
  42. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  43. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  44. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  45. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  46. data/lib/csv_plus_plus/modifier.rb +89 -160
  47. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  48. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  49. data/lib/csv_plus_plus/options/options.rb +97 -0
  50. data/lib/csv_plus_plus/options.rb +34 -77
  51. data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
  52. data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
  53. data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
  54. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  55. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  56. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  57. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  58. data/lib/csv_plus_plus/reader.rb +14 -0
  59. data/lib/csv_plus_plus/row.rb +53 -12
  60. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  61. data/lib/csv_plus_plus/runtime/position.rb +242 -0
  62. data/lib/csv_plus_plus/runtime/references.rb +115 -0
  63. data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
  64. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  65. data/lib/csv_plus_plus/runtime.rb +34 -191
  66. data/lib/csv_plus_plus/source_code.rb +71 -0
  67. data/lib/csv_plus_plus/template.rb +71 -39
  68. data/lib/csv_plus_plus/version.rb +2 -1
  69. data/lib/csv_plus_plus/writer/csv.rb +37 -8
  70. data/lib/csv_plus_plus/writer/excel.rb +25 -5
  71. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
  72. data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
  73. data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
  74. data/lib/csv_plus_plus/writer/merger.rb +31 -0
  75. data/lib/csv_plus_plus/writer/open_document.rb +21 -2
  76. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
  77. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  78. data/lib/csv_plus_plus/writer.rb +79 -10
  79. data/lib/csv_plus_plus.rb +47 -18
  80. metadata +50 -21
  81. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  82. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  83. data/lib/csv_plus_plus/data_validation.rb +0 -138
  84. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
  85. data/lib/csv_plus_plus/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
  87. data/lib/csv_plus_plus/expand.rb +0 -20
  88. data/lib/csv_plus_plus/google_options.rb +0 -27
  89. data/lib/csv_plus_plus/graph.rb +0 -62
  90. data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
  91. data/lib/csv_plus_plus/references.rb +0 -68
  92. data/lib/csv_plus_plus/scope.rb +0 -196
  93. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  94. data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
  95. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
  96. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  97. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -7,32 +7,32 @@
7
7
  require 'racc/parser.rb'
8
8
 
9
9
 
10
- require_relative '../expand'
11
- require_relative '../lexer'
10
+ require_relative '../lexer/racc_lexer'
12
11
 
13
12
  module CSVPlusPlus
14
13
  module Parser
15
14
  class Modifier < Racc::Parser
16
15
 
17
- module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
18
- attr_reader :return_value
16
+ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 60)
17
+ extend ::T::Sig
18
+ extend ::T::Generic
19
+ include ::CSVPlusPlus::Lexer::RaccLexer
19
20
 
20
- include ::CSVPlusPlus::Lexer
21
+ ReturnType = type_member {{ fixed: ::T.nilable(::String) }}
21
22
 
22
23
  # @param cell_modifier [Modifier]
23
24
  # @param row_modifier [Modifier]
24
- # @param scope [Scope]
25
- def initialize(cell_modifier:, row_modifier:, scope:)
25
+ def initialize(cell_modifier:, row_modifier:)
26
26
  super()
27
27
 
28
28
  @parsing_row = false
29
- @cell_modifier = cell_modifier
30
- @row_modifier = row_modifier
31
- @scope = scope
29
+ @cell_modifier = ::CSVPlusPlus::Modifier::ModifierValidator.new(cell_modifier)
30
+ @row_modifier = ::CSVPlusPlus::Modifier::ModifierValidator.new(row_modifier)
32
31
  end
33
32
 
34
33
  protected
35
34
 
35
+ sig { override.params(input: ::String).returns(::T::Boolean) }
36
36
  def anything_to_parse?(input)
37
37
  @modifiers_to_parse = input.scan(/!?\[\[/).count
38
38
 
@@ -44,10 +44,18 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
44
44
  @modifiers_to_parse > 0
45
45
  end
46
46
 
47
+ sig { override.returns(::String) }
47
48
  def parse_subject
48
49
  'modifier'
49
50
  end
50
51
 
52
+ sig { override.returns(ReturnType) }
53
+ # The output of the parser
54
+ def return_value
55
+ @return_value
56
+ end
57
+
58
+ sig { override.returns(::CSVPlusPlus::Lexer::Tokenizer) }
51
59
  def tokenizer
52
60
  ::CSVPlusPlus::Lexer::Tokenizer.new(
53
61
  ignore: /\s+/,
@@ -61,42 +69,43 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
61
69
  @modifiers_to_parse == 0
62
70
  end,
63
71
  tokens: [
64
- [/\bborder\b/, 'border'],
65
- [/\bbordercolor\b/, 'bordercolor'],
66
- [/\bborderstyle\b/, 'borderstyle'],
67
- [/\bcolor\b/, 'color'],
68
- [/\bexpand\b/, 'expand'],
69
- [/\bfontcolor\b/, 'fontcolor'],
70
- [/\bfontfamily\b/, 'fontfamily'],
71
- [/\bfontsize\b/, 'fontsize'],
72
- [/\bformat\b/, 'format'],
73
- [/\bfreeze\b/, 'freeze'],
74
- [/\bhalign\b/, 'halign'],
75
- [/\bnote\b/, 'note'],
76
- [/\bnumberformat\b/, 'numberformat'],
77
- [/\bvalidate\b/, 'validate'],
78
- [/\bvalign\b/, 'valign'],
79
- [/\bvar\b/, 'var'],
80
- [/-?[\d.]+/, :NUMBER],
81
- TOKEN_LIBRARY[:HEX_COLOR],
82
- [
83
- /
72
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bborder\b/, token: 'border'),
73
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bbordercolor\b/, token: 'bordercolor'),
74
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bborderstyle\b/, token: 'borderstyle'),
75
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bcolor\b/, token: 'color'),
76
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bexpand\b/, token: 'expand'),
77
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bfontcolor\b/, token: 'fontcolor'),
78
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bfontfamily\b/, token: 'fontfamily'),
79
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bfontsize\b/, token: 'fontsize'),
80
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bformat\b/, token: 'format'),
81
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bfreeze\b/, token: 'freeze'),
82
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bhalign\b/, token: 'halign'),
83
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bnote\b/, token: 'note'),
84
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bnumberformat\b/, token: 'numberformat'),
85
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bvalidate\b/, token: 'validate'),
86
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bvalign\b/, token: 'valign'),
87
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\bvar\b/, token: 'var'),
88
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /-?[1-9][\d.]*/, token: :NUMBER),
89
+ ::CSVPlusPlus::Lexer::Token.new(
90
+ regexp: /
84
91
  (?:
85
- [\w,_:-] # something that accepts most basic input if it doesn't need to be quoted
86
- [\w\s,_:-]+ # same thing but allow spaces in the middle
87
- [\w,_:-] # no spaces at the end
88
- )
89
- | # - or -
92
+ \w+\s*:\s*'([^'\\]|\\.)*') # allow for a single-quoted string which can accept any input and also allow
93
+ # for escaping via backslash (i.e., 'ain\\'t won\\'t something' is valid)
94
+ | # - or -
95
+ (?:'([^'\\]|\\.)*') # allow for a single-quoted string which can accept any input and also allow
96
+ |
90
97
  (?:
91
- '([^'\\]|\\.)*') # allow for a single-quoted string which can accept any input and also allow
92
- # for escaping via backslash (i.e., 'ain\\'t won\\'t something' is valid)
98
+ [\w,_:-] # something that accepts most basic input if it doesn't need to be quoted
99
+ [\w\s,_:-]+ # same thing but allow spaces in the middle
100
+ [\w,_:-] # no spaces at the end
101
+ )
93
102
  /x,
94
- :RIGHT_SIDE,
95
- ],
96
- [/\[\[/, :START_CELL_MODIFIERS],
97
- [/!\[\[/, :START_ROW_MODIFIERS],
98
- [/\//, :MODIFIER_SEPARATOR],
99
- [/=/, :EQ],
103
+ token: :RIGHT_SIDE,
104
+ ),
105
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\[\[/, token: :START_CELL_MODIFIERS),
106
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /!\[\[/, token: :START_ROW_MODIFIERS),
107
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /\//, token: :MODIFIER_SEPARATOR),
108
+ ::CSVPlusPlus::Lexer::Token.new(regexp: /=/, token: :EQ),
100
109
  ],
101
110
  alter_matches: {
102
111
  STRING: ->(s) { s.gsub(/^'|'$/, '') }
@@ -107,7 +116,7 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
107
116
  private
108
117
 
109
118
  def assign_defaults!
110
- @cell_modifier.take_defaults_from!(@row_modifier)
119
+ @cell_modifier.modifier.take_defaults_from!(@row_modifier.modifier)
111
120
  end
112
121
 
113
122
  def parsing_row!
@@ -123,11 +132,6 @@ module_eval(<<'...end modifier.y/module_eval...', 'modifier.y', 61)
123
132
  assign_defaults!
124
133
  end
125
134
 
126
- def define_var(var_id)
127
- @scope.bind_variable_to_cell(var_id)
128
- modifier.var = var_id.to_sym
129
- end
130
-
131
135
  def modifier
132
136
  @parsing_row ? @row_modifier : @cell_modifier
133
137
  end
@@ -160,7 +164,7 @@ racc_action_pointer = [
160
164
  36, 52, 41, nil, nil, nil, 56, nil, -17, -1,
161
165
  nil, 41, nil, 49, 50, 51, 52, 53, 54, 55,
162
166
  56, 57, nil, 58, 59, 60, 61, 62, 63, 42,
163
- nil, 15, 59, 64, 61, 66, 66, 68, 65, 69,
167
+ nil, 15, 59, 60, 61, 62, 66, 64, 65, 69,
164
168
  67, 68, 69, 70, 71, 72, 73, nil, nil, nil,
165
169
  nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
166
170
  nil, nil, nil, nil ]
@@ -393,7 +397,7 @@ module_eval(<<'.,.,', 'modifier.y', 39)
393
397
 
394
398
  module_eval(<<'.,.,', 'modifier.y', 40)
395
399
  def _reduce_15(val, _values, result)
396
- modifier.expand!
400
+ modifier.infinite_expand!
397
401
  result
398
402
  end
399
403
  .,.,
@@ -456,7 +460,7 @@ module_eval(<<'.,.,', 'modifier.y', 48)
456
460
 
457
461
  module_eval(<<'.,.,', 'modifier.y', 49)
458
462
  def _reduce_24(val, _values, result)
459
- modifier.validation = val[2]
463
+ modifier.validate = val[2]
460
464
  result
461
465
  end
462
466
  .,.,
@@ -470,7 +474,7 @@ module_eval(<<'.,.,', 'modifier.y', 50)
470
474
 
471
475
  module_eval(<<'.,.,', 'modifier.y', 51)
472
476
  def _reduce_26(val, _values, result)
473
- define_var(val[2])
477
+ modifier.var = val[2]
474
478
  result
475
479
  end
476
480
  .,.,
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Reader
6
+ # Reads a CSV file
7
+ class CSV < ::CSVPlusPlus::Reader::Reader
8
+ extend ::T::Sig
9
+ extend ::T::Generic
10
+
11
+ CellValue = type_member { { fixed: ::String } }
12
+ public_constant :CellValue
13
+
14
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions).void }
15
+ # Open a CSV outputter to the +output_filename+ specified by the +Options+
16
+ #
17
+ # @param options [Options] The supplied options.
18
+ def initialize(options)
19
+ super()
20
+
21
+ @options = options
22
+ @cell_values = ::T.let(
23
+ read_csv(@options.output_filename),
24
+ ::T::Array[::T::Array[::T.nilable(::CSVPlusPlus::Reader::CSV::CellValue)]]
25
+ )
26
+ end
27
+
28
+ sig { override.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::CSV::CellValue)) }
29
+ # Get the current value at the +cell+'s location.
30
+ #
31
+ # @param cell [Cell]
32
+ #
33
+ # @return [CellValue, nil]
34
+ def value_at(cell)
35
+ @cell_values[cell.row_index]&.[](cell.index)
36
+ end
37
+
38
+ protected
39
+
40
+ sig do
41
+ params(filename: ::Pathname).returns(::T::Array[::T::Array[::T.nilable(::CSVPlusPlus::Reader::CSV::CellValue)]])
42
+ end
43
+ def read_csv(filename)
44
+ return [[]] unless ::File.exist?(filename)
45
+
46
+ ::CSV.read(filename.to_s)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,129 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Reader
6
+ # A class that can read an existing Google Sheets spreadsheet
7
+ class GoogleSheets < ::CSVPlusPlus::Reader::Reader
8
+ extend ::T::Sig
9
+ extend ::T::Generic
10
+ include ::CSVPlusPlus::GoogleApiClient
11
+
12
+ CellValue = type_member { { fixed: ::String } }
13
+ public_constant :CellValue
14
+
15
+ sig { params(options: ::CSVPlusPlus::Options::GoogleSheetsOptions).void }
16
+ # Open a CSV outputter to the +output_filename+ specified by the +Options+
17
+ #
18
+ # @param options [Options::GoogleSheetsOptions] The supplied options.
19
+ def initialize(options)
20
+ super()
21
+
22
+ @options = options
23
+ @cell_values = ::T.let(nil, ::T.nilable(::T::Array[::T::Array[::T.nilable(::String)]]))
24
+ end
25
+
26
+ sig do
27
+ override.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::GoogleSheets::CellValue))
28
+ end
29
+ # Get the current value at the +cell+'s location.
30
+ #
31
+ # @param cell [Cell]
32
+ #
33
+ # @return [CellValue, nil]
34
+ def value_at(cell)
35
+ cell_values[cell.row_index]&.[](cell.index)
36
+ end
37
+
38
+ sig { returns(::T.nilable(::Google::Apis::SheetsV4::Sheet)) }
39
+ # @return [Google::Apis::SheetsV4::Sheet, nil]
40
+ def sheet
41
+ spreadsheet.sheets.find { |s| s.properties.title.strip == @options.sheet_name.strip }
42
+ end
43
+
44
+ sig { returns(::Google::Apis::SheetsV4::Spreadsheet) }
45
+ # @return [Google::Apis::SheetsV4::Spreadsheet]
46
+ def spreadsheet
47
+ @spreadsheet ||= ::T.let(
48
+ sheets_client.get_spreadsheet(@options.sheet_id),
49
+ ::T.nilable(::Google::Apis::SheetsV4::Spreadsheet)
50
+ )
51
+
52
+ unless @spreadsheet
53
+ raise(::CSVPlusPlus::Error::WriterError, "Unable to connect to google spreadsheet #{@options.sheet_id}")
54
+ end
55
+
56
+ @spreadsheet
57
+ end
58
+
59
+ private
60
+
61
+ sig { returns(::T::Array[::T::Array[::T.nilable(::String)]]) }
62
+ # rubocop:disable Metrics/MethodLength
63
+ def cell_values
64
+ return @cell_values if @cell_values
65
+
66
+ formatted_values = get_all_spreadsheet_values('FORMATTED_VALUE')
67
+ formula_values = get_all_spreadsheet_values('FORMULA')
68
+
69
+ @cell_values =
70
+ ::T.must(
71
+ ::T.let(
72
+ if formula_values&.values.nil? || formatted_values&.values.nil?
73
+ []
74
+ else
75
+ extract_current_values(::T.must(formatted_values), ::T.must(formula_values))
76
+ end,
77
+ ::T.nilable(::T::Array[::T::Array[::T.nilable(::String)]])
78
+ )
79
+ )
80
+ end
81
+ # rubocop:enable Metrics/MethodLength
82
+
83
+ sig { params(render_option: ::String).returns(::T.nilable(::Google::Apis::SheetsV4::ValueRange)) }
84
+ def get_all_spreadsheet_values(render_option)
85
+ sheets_client.get_spreadsheet_values(@options.sheet_id, full_range, value_render_option: render_option)
86
+ rescue ::Google::Apis::ClientError => e
87
+ return if e.status_code == 404
88
+
89
+ raise
90
+ end
91
+
92
+ sig do
93
+ params(
94
+ formatted_values: ::Google::Apis::SheetsV4::ValueRange,
95
+ formula_values: ::Google::Apis::SheetsV4::ValueRange
96
+ ).returns(::T::Array[::T::Array[::T.nilable(::String)]])
97
+ end
98
+ def extract_current_values(formatted_values, formula_values)
99
+ formatted_values.values.map.each_with_index do |row, x|
100
+ row.map.each_with_index do |_cell, y|
101
+ formula_value = formula_values.values[x][y]
102
+ if formula_value.is_a?(::String) && formula_value.start_with?('=')
103
+ formula_value
104
+ else
105
+ strip_to_nil(formatted_values.values[x][y])
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ sig { params(range: ::String).returns(::String) }
112
+ def format_range(range)
113
+ # "'#{@options.sheet_name}'!#{range}"
114
+ # XXX
115
+ range
116
+ end
117
+
118
+ sig { returns(::String) }
119
+ def full_range
120
+ format_range('A1:Z1000')
121
+ end
122
+
123
+ sig { params(str: ::String).returns(::T.nilable(::String)) }
124
+ def strip_to_nil(str)
125
+ str.strip.empty? ? nil : str
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Reader
6
+ # +Reader+s are used to complement +Writer+ instances by reading in the existing spreadsheet and providing
7
+ # a way to merge in results.
8
+ class Reader
9
+ extend ::T::Sig
10
+ extend ::T::Generic
11
+ extend ::T::Helpers
12
+
13
+ abstract!
14
+
15
+ CellValue = type_member
16
+ public_constant :CellValue
17
+
18
+ sig { abstract.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::Reader::CellValue)) }
19
+ # Get the current value at the +cell+'s location.
20
+ #
21
+ # @param cell [Cell]
22
+ #
23
+ # @return [CellValue, nil]
24
+ def value_at(cell); end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Reader
6
+ # Reads an Excel file
7
+ class RubyXL < ::CSVPlusPlus::Reader::Reader
8
+ extend ::T::Sig
9
+ extend ::T::Generic
10
+
11
+ CellValue = type_member { { fixed: ::RubyXL::Cell } }
12
+ public_constant :CellValue
13
+
14
+ sig { params(options: ::CSVPlusPlus::Options::FileOptions, worksheet: ::RubyXL::Worksheet).void }
15
+ # Open an excel outputter to the +output_filename+ specified by the +Options+
16
+ #
17
+ # @param options [Options] The supplied options.
18
+ # @param worksheet [RubyXL::Worksheet] The already-opened RubyXL worksheet
19
+ def initialize(options, worksheet)
20
+ super()
21
+
22
+ @options = options
23
+ @worksheet = worksheet
24
+ end
25
+
26
+ sig { override.params(cell: ::CSVPlusPlus::Cell).returns(::T.nilable(::CSVPlusPlus::Reader::RubyXL::CellValue)) }
27
+ # Get the current value at the +cell+'s position
28
+ #
29
+ # @param cell [Cell]
30
+ #
31
+ # @return [RubyXL::Cell, nil]
32
+ def value_at(cell)
33
+ @worksheet.sheet_data[cell.row_index]&.[](cell.index)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # Classes which can read spreadsheets in our various formats.
6
+ module Reader
7
+ end
8
+ end
9
+
10
+ require_relative './reader/reader'
11
+
12
+ require_relative './reader/csv'
13
+ require_relative './reader/google_sheets'
14
+ require_relative './reader/rubyxl'
@@ -1,23 +1,37 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
- # A row of a template
5
+ # A row of a template. A row contains an +Array+ of +Cell+s and possibly a row-level +Modifier+.
5
6
  #
6
- # @attr_reader cells [Array<Cell>]
7
- # @attr_reader index [Integer] The index of this row
7
+ # @attr_reader cells [Array<Cell>] The cells contained by this row.
8
+ # @attr_reader index [Integer] The index of this row. Starts at 0.
8
9
  # @attr_reader modifier [Modifier] The modifier to apply to all cells in this row
9
10
  class Row
10
- attr_reader :cells, :index, :modifier
11
+ extend ::T::Sig
11
12
 
12
- # @param index [Integer] The index of this row (starts at 0)
13
+ sig { returns(::T::Array[::CSVPlusPlus::Cell]) }
14
+ attr_reader :cells
15
+
16
+ sig { returns(::Integer) }
17
+ attr_reader :index
18
+
19
+ sig { returns(::CSVPlusPlus::Modifier::Modifier) }
20
+ attr_reader :modifier
21
+
22
+ sig do
23
+ params(cells: ::T::Array[::CSVPlusPlus::Cell], index: ::Integer, modifier: ::CSVPlusPlus::Modifier::Modifier).void
24
+ end
13
25
  # @param cells [Array<Cell>] The cells belonging to this row
26
+ # @param index [Integer] The index of this row (starts at 0)
14
27
  # @param modifier [Modifier] The modifier to apply to all cells in this row
15
- def initialize(index, cells, modifier)
28
+ def initialize(cells:, index:, modifier:)
16
29
  @cells = cells
17
30
  @modifier = modifier
18
31
  @index = index
19
32
  end
20
33
 
34
+ sig { params(index: ::Integer).void }
21
35
  # Set the row's +index+ and update the +row_index+ of all affected cells
22
36
  #
23
37
  # @param index [Integer] The index of this row (starts at 0)
@@ -26,25 +40,52 @@ module CSVPlusPlus
26
40
  @cells.each { |cell| cell.row_index = index }
27
41
  end
28
42
 
43
+ sig { returns(::Integer) }
29
44
  # How much this row will expand itself, if at all (0)
30
45
  #
31
46
  # @return [Integer]
32
47
  def expand_amount
33
- return 0 unless @modifier.expand
48
+ return 0 if @modifier.expand.nil?
34
49
 
35
- @modifier.expand.repetitions || (1000 - @index)
50
+ ::T.must(@modifier.expand).repetitions || (1000 - @index)
36
51
  end
37
52
 
38
- # @return [String]
39
- def to_s
40
- "Row(index: #{index}, modifier: #{modifier}, cells: #{cells})"
53
+ sig { params(starts_at: ::Integer, into: ::T::Array[::CSVPlusPlus::Row]).returns(::T::Array[::CSVPlusPlus::Row]) }
54
+ # Starting at +starts_at+, do a deep copy of this row into the +Array+ referenced by +into+.
55
+ #
56
+ # @param starts_at [Integer] The +row_index+ where this row was expanded.
57
+ # @param into [Array<Row>] An array where the expanded rows will be accumulated.
58
+ #
59
+ # @return [Array<Row>] The rows expanded
60
+ def expand_rows(starts_at:, into: [])
61
+ return into if @modifier.expand.nil?
62
+
63
+ ::T.must(@modifier.expand).starts_at = starts_at
64
+
65
+ starts_at.upto(expand_amount + starts_at - 1) do |row_index|
66
+ into << deep_clone.tap { |c| c.index = row_index }
67
+ end
68
+ into
41
69
  end
42
70
 
71
+ sig { returns(::T::Boolean) }
72
+ # Does the row have an ![[expand]] modifier but is yet to be expanded?
73
+ #
74
+ # @return [T::Boolean]
75
+ def unexpanded?
76
+ return false if @modifier.expand.nil?
77
+
78
+ !::T.must(@modifier.expand).expanded?
79
+ end
80
+
81
+ private
82
+
83
+ sig { returns(::CSVPlusPlus::Row) }
43
84
  # Return a deep copy of this row
44
85
  #
45
86
  # @return [Row]
46
87
  def deep_clone
47
- ::Marshal.load(::Marshal.dump(self))
88
+ ::T.cast(::Marshal.load(::Marshal.dump(self)), ::CSVPlusPlus::Row)
48
89
  end
49
90
  end
50
91
  end
@@ -0,0 +1,68 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'tsort'
5
+
6
+ module CSVPlusPlus
7
+ module Runtime
8
+ # Graph ordering and searching functions
9
+ module Graph
10
+ # Get a list of all variables references in a given +ast+
11
+ # TODO: this is only used in one place - refactor it
12
+ def self.variable_references(ast, include_runtime_variables: false)
13
+ depth_first_search(ast) do |node|
14
+ next unless node.is_a?(::CSVPlusPlus::Entities::Reference)
15
+
16
+ node.id if !::CSVPlusPlus::Entities::Builtins.builtin_variable?(node.id) || include_runtime_variables
17
+ end
18
+ end
19
+
20
+ # Create a dependency graph of +variables+
21
+ def self.dependency_graph(variables)
22
+ ::CSVPlusPlus::Runtime::Graph::DependencyGraph[
23
+ variables.map { |var_id, ast| [var_id, variable_references(ast)] }
24
+ ]
25
+ end
26
+
27
+ # TODO: I don't think we use this anymore - it was useful when I wanted to resolve variables in their dependency
28
+ # order
29
+ #
30
+ # Perform a topological sort on a +DependencyGraph+. A toplogical sort is noteworthy
31
+ # because it will give us the order in which we need to resolve our variable dependencies.
32
+ #
33
+ # Given this dependency graph:
34
+ #
35
+ # { a: [b c], b: [c], c: [d], d: [] }
36
+ #
37
+ # it will return:
38
+ #
39
+ # [d, c, b, a]
40
+ #
41
+ def self.topological_sort(dependencies)
42
+ dependencies.tsort
43
+ end
44
+
45
+ # Do a DFS on an AST starting at +node+
46
+ def self.depth_first_search(node, accum = [], &)
47
+ ret = yield(node)
48
+ accum << ret unless ret.nil?
49
+
50
+ return accum unless node.is_a?(::CSVPlusPlus::Entities::FunctionCall)
51
+
52
+ node.arguments.each { |n| depth_first_search(n, accum, &) }
53
+ accum
54
+ end
55
+
56
+ # A dependency graph represented as a +Hash+ which will be used by our +topological_sort+ function
57
+ class DependencyGraph < Hash
58
+ include ::TSort
59
+ alias tsort_each_node each_key
60
+
61
+ # sort each child
62
+ def tsort_each_child(node, &)
63
+ fetch(node).each(&)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end