csv_plus_plus 0.1.2 → 0.2.0

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