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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0fb79a1f7a00d60f77289518fb9c6ccac6b059024d5cf66a3f083ec92e77def
4
- data.tar.gz: a35796b982b01171636e27b1d1a9d19d6d414249b0e4634947d5a754cb240146
3
+ metadata.gz: a4462a2a82490271a95479970e10246ed7d8b1e8a62d21928a5f2b710b2bd511
4
+ data.tar.gz: 31ce301945c5cc4d3154395dfa62271637baba4af78d2efed9b13d779036e015
5
5
  SHA512:
6
- metadata.gz: 45dd804f7889d65ac5f5c63ccc131d774e7ea24097bfe9449af63a48345ca0b9f16ea9e84c17a73151c9ee8d9d90ce4b01d67b7ced817e831e66781d2e7141e0
7
- data.tar.gz: 21cda263af6d05d5ee396a9d00b4c1c78f1a043d91787dcf0d822e1adb6a97a2326da836cdc3bbc3c78e43e5c342c1bdb9da966884f1d9d8df364e574f334ded
6
+ metadata.gz: a968bc15a47a6ac129b0a4a13df3dfb1997991548f13af9ac28598091b5562be8d7d1943a03f86a1a501533ff2eb974d1e2cc788f43fca27f09be455edcdba9f
7
+ data.tar.gz: f091403bede1fb2e751fad9b6937a836f0676e5ab284690fa2c8983fa746d90e9780c701d3a7ca1161fa9aca04b84263a1b7e7789dda866d6fe1caf975593287
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ ![main](https://github.com/patrickomatic/csv-plus-plus/actions/workflows/rspec.yml/badge.svg)
1
2
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-community-brightgreen.svg)](https://rubystyle.guide)
2
3
  [![Gem Version](https://badge.fury.io/rb/csv_plus_plus.svg)](https://badge.fury.io/rb/csv_plus_plus)
3
4
 
@@ -19,8 +20,8 @@ quantity := celladjacent(D)
19
20
  def profit() (price * quantity) - fees
20
21
 
21
22
  ---
22
- ![[format=bold/align=center]]Date,Ticker,Price,Quantity,Total,Fees
23
- ![[expand]],[[format=bold]],,,"=profit()",$$fees
23
+ ![[format=bold/align=center]]Date ,Ticker ,Price ,Quantity ,Profit ,Fees
24
+ ![[expand]] ,[[format=italic]] , , ,"=profit()" ,=fees
24
25
  ```
25
26
 
26
27
  And can be compiled into a `.xlsx` file by:
@@ -29,7 +30,6 @@ And can be compiled into a `.xlsx` file by:
29
30
  $ csv++ -n 'My Stock Tracker' -o mystocks.xlsx mystocks.csvpp
30
31
  ```
31
32
 
32
-
33
33
  See the [Language Reference](./docs/LANGUAGE_REFERENCE.md) for a full explanation of features.
34
34
 
35
35
  ## Installing
@@ -38,7 +38,7 @@ Just install it via rubygems (homebrew and debian packages are in the works):
38
38
 
39
39
  `$ gem install csv_plus_plus`
40
40
 
41
- or if you want the very latest changes, clone the repository and run:
41
+ or if you want the very latest changes, clone this repository and run:
42
42
 
43
43
  `$ rake gem:install`
44
44
 
@@ -46,7 +46,7 @@ or if you want the very latest changes, clone the repository and run:
46
46
 
47
47
  ## Examples
48
48
 
49
- Take a look at the [examples](./examples/) directory for a bunch of example `.csvpp` files.
49
+ Take a look at the [repository of examples](https://github.com/patrickomatic/csvpp-examples) repository for a bunch of example `.csvpp` files.
50
50
 
51
51
  ## CLI Arguments
52
52
 
@@ -63,3 +63,7 @@ Usage: csv++ [options]
63
63
  -x, --offset-columns OFFSET Apply the template offset by OFFSET cells
64
64
  -y, --offset-rows OFFSET Apply the template offset by OFFSET rows
65
65
  ```
66
+
67
+ ## See Also:
68
+
69
+ * [Supported features by output format](./docs/feature_matrix.csvpp)
@@ -1,3 +1,28 @@
1
+ ## main/upcoming
2
+
3
+ ## v0.2.0
4
+
5
+ ### **Breaking Changes**
6
+
7
+ - Removal of the $$ operator - to dereference variables you can just reference them by name and they will be resolved if they are defined. Otherwise they will be left alone in the output
8
+
9
+ ### Non-breaking Changes
10
+
11
+ - Excel: fix the merging of existing values
12
+ - CSV: fix the merging of existing values
13
+ - Support merging in values from CSV (previously it would ignore/overwrite them)
14
+ - Allow for more generous spacing in the csv section (and reflect this in the examples)
15
+ - More type coverage
16
+
17
+ ## v0.1.3
18
+
19
+ - Proper scoping of variables defined within an expand modifier
20
+ - Types via Sorbet
21
+ - Fix formula insertion on Excel
22
+ - Fix modifier string quoting
23
+ - Fix broken Yard doc generation
24
+ - Fix: multiple modifiers on the same row weren't being handled
25
+
1
26
  ## v0.1.2
2
27
 
3
28
  - var=... modifier which allows binding a variable to a cell
@@ -0,0 +1,202 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # A reference to a cell. Internally it is represented by a simple +cell_index+ and +row_index+ but there are
6
+ # functions for converting to and from A1-style formats. Supported formats are:
7
+ #
8
+ # * `1` - A reference to the entire first row
9
+ # * `A` - A reference to the entire first column
10
+ # * `A1` - A reference to the first cell (top left)
11
+ # * `A1:D10` - The range defined between A1 and D10
12
+ # * `Sheet1!B2` - Cell B2 on the sheet "Sheet1"
13
+ #
14
+ # @attr sheet_name [String, nil] The name of the sheet reference
15
+ # @attr_reader cell_index [Integer, nil] The cell index of the cell being referenced
16
+ # @attr_reader row_index [Integer, nil] The row index of the cell being referenced
17
+ # @attr_reader upper_cell_index [Integer, nil] If set, the cell reference is a range and this is the upper cell
18
+ # index of it
19
+ # @attr_reader upper_row_index [Integer, nil] If set, the cell reference is a range and this is the upper row index
20
+ # of in
21
+ # rubocop:disable Metrics/ClassLength
22
+ class A1Reference
23
+ extend ::T::Sig
24
+
25
+ sig { returns(::T.nilable(::String)) }
26
+ attr_accessor :sheet_name
27
+
28
+ sig { returns(::T.nilable(::Integer)) }
29
+ attr_reader :cell_index
30
+
31
+ sig { returns(::T.nilable(::Integer)) }
32
+ attr_reader :row_index
33
+
34
+ sig { returns(::T.nilable(::Integer)) }
35
+ attr_reader :upper_cell_index
36
+
37
+ sig { returns(::T.nilable(::Integer)) }
38
+ attr_reader :upper_row_index
39
+
40
+ sig { returns(::T.nilable(::CSVPlusPlus::Modifier::Expand)) }
41
+ attr_reader :scoped_to_expand
42
+
43
+ # TODO: this is getting gross, maybe define an actual parser
44
+ A1_NOTATION_REGEXP = /
45
+ ^
46
+ (?:
47
+ (?:
48
+ (?:'([^'\\]|\\.)*') # allow for a single-quoted sheet name
49
+ |
50
+ (\w+) # or if it's not quoted, just allow \w+
51
+ )
52
+ ! # if a sheet name is specified, it's always followed by a !
53
+ )?
54
+ ([a-zA-Z0-9]+) # the only part required - something alphanumeric
55
+ (?: :([a-zA-Z0-9]+))? # and they might make it a range
56
+ $
57
+ /x
58
+ public_constant :A1_NOTATION_REGEXP
59
+
60
+ ALPHA = ::T.let(('A'..'Z').to_a.freeze, ::T::Array[::String])
61
+ private_constant :ALPHA
62
+
63
+ sig { params(cell_reference_string: ::String).returns(::T::Boolean) }
64
+ # Does the given +cell_reference_string+ conform to a valid cell reference?
65
+ #
66
+ # {https://developers.google.com/sheets/api/guides/concepts}
67
+ #
68
+ # @param cell_reference_string [::String] The string to check if it is a valid cell reference (we assume it's in
69
+ # A1 notation but maybe can support R1C1)
70
+ #
71
+ # @return [::T::Boolean]
72
+ def self.valid_cell_reference?(cell_reference_string)
73
+ !(cell_reference_string =~ ::CSVPlusPlus::A1Reference::A1_NOTATION_REGEXP).nil?
74
+ end
75
+
76
+ sig do
77
+ params(
78
+ ref: ::T.nilable(::String),
79
+ cell_index: ::T.nilable(::Integer),
80
+ row_index: ::T.nilable(::Integer),
81
+ scoped_to_expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)
82
+ ).void
83
+ end
84
+ # Either +ref+, +cell_index+ or +row_index+ must be specified. If +ref+ is supplied it will be parsed to calculate
85
+ # +row_index+ and +cell_index.
86
+ #
87
+ # @param ref [String, nil] A raw user-inputted reference
88
+ # @param cell_index [Integer, nil] The index of the cell being referenced.
89
+ # @param row_index [Integer, nil] The index of the row being referenced.
90
+ # @param scoped_to_expand [Expand] The [[expand]] that this cell reference will be scoped to. In other words, it
91
+ # will only be able to be resolved if the position is within the bounds of the expand (it can't be referenced
92
+ # outside of the expand.)
93
+ def initialize(ref: nil, cell_index: nil, row_index: nil, scoped_to_expand: nil)
94
+ raise(::ArgumentError, 'Must specify :cell_index or :row_index') unless ref || cell_index || row_index
95
+
96
+ @scoped_to_expand = scoped_to_expand
97
+
98
+ if ref
99
+ from_a1_ref!(ref)
100
+ else
101
+ @cell_index = ::T.let(cell_index, ::T.nilable(::Integer))
102
+ @row_index = ::T.let(row_index, ::T.nilable(::Integer))
103
+
104
+ @upper_cell_index = ::T.let(nil, ::T.nilable(::Integer))
105
+ @upper_row_index = ::T.let(nil, ::T.nilable(::Integer))
106
+ end
107
+ end
108
+
109
+ sig { override.params(other: ::BasicObject).returns(::T::Boolean) }
110
+ # @param other [BasicObject]
111
+ #
112
+ # @return [boolean]
113
+ def ==(other)
114
+ case other
115
+ when self.class
116
+ @cell_index == other.cell_index && @row_index == other.row_index && @sheet_name == other.sheet_name \
117
+ && @upper_cell_index == other.upper_cell_index && @upper_row_index == other.upper_row_index
118
+ else
119
+ false
120
+ end
121
+ end
122
+
123
+ sig { params(position: ::CSVPlusPlus::Runtime::Position).returns(::T.nilable(::String)) }
124
+ # Turns index-based/X,Y coordinates into a A1 format
125
+ #
126
+ # @param position [Position]
127
+ #
128
+ # @return [::String, nil]
129
+ def to_a1_ref(position)
130
+ row_index = position_row_index(position)
131
+ return unless row_index || @cell_index
132
+
133
+ rowref = row_index ? (row_index + 1).to_s : ''
134
+ cellref = @cell_index ? to_a1_cell_ref : ''
135
+ [cellref, rowref].join
136
+ end
137
+
138
+ private
139
+
140
+ sig { params(position: ::CSVPlusPlus::Runtime::Position).returns(::T.nilable(::Integer)) }
141
+ def position_row_index(position)
142
+ @scoped_to_expand ? position.row_index : @row_index
143
+ end
144
+
145
+ sig { returns(::String) }
146
+ # Turns a cell index into an A1 reference (just the "A" part - for example 0 == 'A', 1 == 'B', 2 == 'C', etc.)
147
+ #
148
+ # @return [::String]
149
+ def to_a1_cell_ref
150
+ c = @cell_index.dup
151
+ ref = ''
152
+
153
+ while c >= 0
154
+ # rubocop:disable Lint/ConstantResolution
155
+ ref += ::T.must(ALPHA[c % 26])
156
+ # rubocop:enable Lint/ConstantResolution
157
+ c = (c / 26).floor - 1
158
+ end
159
+
160
+ ref.reverse
161
+ end
162
+
163
+ sig { params(ref: ::String).void }
164
+ def from_a1_ref!(ref)
165
+ quoted_sheet_name, unquoted_sheet_name, lower_range, upper_range = ::T.must(
166
+ ref.strip.match(
167
+ ::CSVPlusPlus::A1Reference::A1_NOTATION_REGEXP
168
+ )
169
+ ).captures
170
+
171
+ @sheet_name = quoted_sheet_name || unquoted_sheet_name
172
+
173
+ parse_lower_range!(lower_range) if lower_range
174
+ parse_upper_range!(upper_range) if upper_range
175
+ end
176
+
177
+ sig { params(lower_range: ::String).void }
178
+ def parse_lower_range!(lower_range)
179
+ cell_ref, row_ref = ::T.must(lower_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
180
+ @cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
181
+ @row_index = Integer(row_ref, 10) - 1 if row_ref
182
+ end
183
+
184
+ sig { params(upper_range: ::String).void }
185
+ # TODO: make this less redundant with the above function
186
+ def parse_upper_range!(upper_range)
187
+ cell_ref, row_ref = ::T.must(upper_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
188
+ @upper_cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
189
+ @upper_row_index = Integer(row_ref, 10) - 1 if row_ref
190
+ end
191
+
192
+ sig { params(cell_ref: ::String).returns(::Integer) }
193
+ def from_a1_cell_ref!(cell_ref)
194
+ (cell_ref.upcase.chars.reduce(0) do |cell_index, letter|
195
+ # rubocop:disable Lint/ConstantResolution
196
+ (cell_index * 26) + ::T.must(ALPHA.find_index(letter)) + 1
197
+ # rubocop:enable Lint/ConstantResolution
198
+ end) - 1
199
+ end
200
+ end
201
+ # rubocop:enable Metrics/ClassLength
202
+ end
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -5,57 +6,106 @@ module CSVPlusPlus
5
6
  #
6
7
  # @attr_reader timings [Array<Benchmark::Tms>] +Benchmark+ timings that have been accumulated by each step of
7
8
  # compilation
8
- # @attr_reader benchmark [Benchmark] A +Benchmark+ instance
9
- module BenchmarkedCompiler
10
- attr_reader :benchmark, :timings
9
+ # @attr_reader benchmark [Benchmark::Report] A +Benchmark+ instance
10
+ class BenchmarkedCompiler < ::CSVPlusPlus::Compiler
11
+ extend ::T::Sig
11
12
 
12
- # Wrap a +Compiler+ with our instance methods that add benchmarks
13
- def self.with_benchmarks(compiler, &block)
14
- ::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
15
- # compiler = new(options:, runtime:, benchmark: x)
16
- compiler.extend(self)
17
- compiler.benchmark = x
13
+ sig { returns(::Benchmark::Report) }
14
+ attr_reader :benchmark
18
15
 
19
- block.call(compiler)
16
+ sig { returns(::T::Array[::Benchmark::Tms]) }
17
+ attr_reader :timings
20
18
 
19
+ sig do
20
+ params(
21
+ options: ::CSVPlusPlus::Options::Options,
22
+ runtime: ::CSVPlusPlus::Runtime::Runtime,
23
+ block: ::T.proc.params(compiler: ::CSVPlusPlus::Compiler).void
24
+ ).void
25
+ end
26
+ # Instantiate a +::Compiler+ that can benchmark (time) it's stages. For better or worse, the only way that they
27
+ # Benchmark library exposes it's +::Benchmark::Report+ is via a block, so this code also has to wrap with one
28
+ #
29
+ # @param options [Options]
30
+ # @param runtime [Runtime]
31
+ def self.with_benchmarks(options:, runtime:, &block)
32
+ ::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
33
+ # compiler.extend(self)
34
+ compiler = new(benchmark: x, options:, runtime:)
35
+ block.call(compiler)
21
36
  [compiler.timings.reduce(:+)]
22
37
  end
23
38
  end
24
39
 
25
- # @param benchmark [Benchmark] A +Benchmark+ instance
26
- def benchmark=(benchmark)
27
- @benchmark = benchmark
28
- @timings = []
40
+ sig do
41
+ params(
42
+ benchmark: ::Benchmark::Report,
43
+ options: ::CSVPlusPlus::Options::Options,
44
+ runtime: ::CSVPlusPlus::Runtime::Runtime
45
+ ).void
29
46
  end
47
+ # @param benchmark [::Benchmark::Report]
48
+ def initialize(benchmark:, options:, runtime:)
49
+ super(options:, runtime:)
30
50
 
51
+ @benchmark = ::T.let(benchmark, ::Benchmark::Report)
52
+ @timings = ::T.let([], ::T::Array[::Benchmark::Tms])
53
+ end
54
+
55
+ sig { params(block: ::T.proc.params(position: ::CSVPlusPlus::Runtime::Position).void).void }
31
56
  # Time the Compiler#outputting! stage
32
- def outputting!
33
- time_stage('Writing the spreadsheet') { super }
57
+ # rubocop:disable Naming/BlockForwarding
58
+ def outputting!(&block)
59
+ time_stage('Writing the spreadsheet') { super(&block) }
34
60
  end
61
+ # rubocop:enable Naming/BlockForwarding
35
62
 
36
63
  protected
37
64
 
65
+ sig { override.void }
38
66
  def parse_code_section!
39
67
  time_stage('Parsing code section') { super }
40
68
  end
41
69
 
70
+ sig { override.returns(::T::Array[::CSVPlusPlus::Row]) }
42
71
  def parse_csv_section!
43
72
  time_stage('Parsing CSV section') { super }
44
73
  end
45
74
 
46
- def expanding
47
- time_stage('Expanding rows') { super }
75
+ sig { override.params(block: ::T.proc.void).void }
76
+ # rubocop:disable Naming/BlockForwarding
77
+ def expanding!(&block)
78
+ time_stage('Expanding rows') { super(&block) }
79
+ end
80
+ # rubocop:enable Naming/BlockForwarding
81
+
82
+ sig { override.params(block: ::T.proc.void).void }
83
+ # rubocop:disable Naming/BlockForwarding
84
+ def bind_all_vars!(&block)
85
+ time_stage('Binding [[var=]]') { super(&block) }
48
86
  end
87
+ # rubocop:enable Naming/BlockForwarding
49
88
 
89
+ sig do
90
+ override
91
+ .params(template: ::CSVPlusPlus::Template)
92
+ .returns(::T::Array[::T::Array[::CSVPlusPlus::Entities::Entity]])
93
+ end
50
94
  def resolve_all_cells!(template)
51
95
  time_stage('Resolving each cell') { super(template) }
52
96
  end
53
97
 
54
98
  private
55
99
 
100
+ sig do
101
+ type_parameters(:R).params(
102
+ stage: ::String,
103
+ block: ::T.proc.returns(::T.type_parameter(:R))
104
+ ).returns(::T.nilable(::T.type_parameter(:R)))
105
+ end
56
106
  def time_stage(stage, &block)
57
- ret = nil
58
- @timings << @benchmark.report(stage) { ret = block.call }
107
+ ret = ::T.let(nil, ::T.nilable(::T.type_parameter(:R)))
108
+ @timings << ::T.unsafe(@benchmark.report(stage) { ret = block.call })
59
109
  ret
60
110
  end
61
111
  end
@@ -1,67 +1,55 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative 'modifier'
4
- require_relative 'parser/cell_value.tab'
5
-
6
4
  module CSVPlusPlus
7
5
  # A cell of a template
8
6
  #
9
- # @attr ast [Entity]
7
+ # @attr ast [Entity, nil] The AST of the formula in the cell (if there is one)
10
8
  # @attr row_index [Integer] The cell's row index (starts at 0)
11
9
  # @attr_reader index [Integer] The cell's index (starts at 0)
12
10
  # @attr_reader modifier [Modifier] The modifier for this cell
13
11
  class Cell
14
- attr_accessor :ast, :row_index
15
- attr_reader :index, :modifier
12
+ extend ::T::Sig
16
13
 
17
- # Parse a +value+ into a Cell object.
18
- #
19
- # @param value [String] A string value which should already have been processed through a CSV parser
20
- # @param runtime [Runtime]
21
- # @param modifier [Modifier]
22
- #
23
- # @return [Cell]
24
- def self.parse(value, runtime:, modifier:)
25
- new(value:, row_index: runtime.row_index, index: runtime.cell_index, modifier:).tap do |c|
26
- c.ast = ::CSVPlusPlus::Parser::CellValue.new.parse(value, runtime)
27
- end
28
- end
14
+ sig { returns(::T.nilable(::CSVPlusPlus::Entities::Entity)) }
15
+ attr_accessor :ast
29
16
 
30
- # @param row_index [Integer] The cell's row index (starts at 0)
17
+ sig { returns(::Integer) }
18
+ attr_accessor :row_index
19
+
20
+ sig { returns(::Integer) }
21
+ attr_reader :index
22
+
23
+ sig { returns(::CSVPlusPlus::Modifier::Modifier) }
24
+ attr_reader :modifier
25
+
26
+ sig do
27
+ params(
28
+ index: ::Integer,
29
+ modifier: ::CSVPlusPlus::Modifier::Modifier,
30
+ row_index: ::Integer,
31
+ value: ::T.nilable(::String)
32
+ ).void
33
+ end
31
34
  # @param index [Integer] The cell's index (starts at 0)
32
- # @param value [String] A string value which should already have been processed through a CSV parser
33
35
  # @param modifier [Modifier] A modifier to apply to this cell
34
- def initialize(row_index:, index:, value:, modifier:)
36
+ # @param row_index [Integer] The cell's row index (starts at 0)
37
+ # @param value [String] A string value which should already have been processed through a CSV parser
38
+ def initialize(index:, modifier:, row_index:, value:)
35
39
  @value = value
36
40
  @modifier = modifier
37
41
  @index = index
38
42
  @row_index = row_index
39
43
  end
40
44
 
45
+ sig { returns(::T.nilable(::String)) }
41
46
  # The +@value+ (cleaned up some)
42
47
  #
43
- # @return [String]
48
+ # @return [::String]
44
49
  def value
45
- return if @value.nil? || @value.strip.empty?
46
-
47
- @value.strip
48
- end
49
-
50
- # @return [String]
51
- def to_s
52
- "Cell(index: #{@index}, row_index: #{@row_index}, value: #{@value}, modifier: #{@modifier})"
53
- end
54
-
55
- # A compiled final representation of the cell. This can only happen after all cell have had
56
- # variables and functions resolved.
57
- #
58
- # @return [String]
59
- def to_csv
60
- return value unless @ast
50
+ stripped = @value&.strip
61
51
 
62
- # This looks really simple but we're relying on each node of the AST to define #to_s such that calling
63
- # this at the top will recursively print the tree (as a well-formatted spreadsheet formula)
64
- "=#{@ast}"
52
+ stripped&.empty? ? nil : stripped
65
53
  end
66
54
  end
67
55
  end
@@ -1,103 +1,48 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require 'optparse'
4
-
5
4
  module CSVPlusPlus
6
- # Handle running the application with the given CLI flags
5
+ # Handle running the application with the supported +CLIFlag+s
7
6
  #
8
- # @attr options [Options, nil] The parsed CLI options
7
+ # @attr options [Options] The parsed CLI options
9
8
  class CLI
9
+ extend ::T::Sig
10
+
11
+ sig { returns(::CSVPlusPlus::Options::Options) }
10
12
  attr_accessor :options
11
13
 
14
+ sig { returns(::CSVPlusPlus::SourceCode) }
15
+ attr_accessor :source_code
16
+
17
+ sig { void }
12
18
  # Handle CLI flags and launch the compiler
13
19
  #
14
20
  # @return [CLI]
15
21
  def self.launch_compiler!
16
- cli = new
17
- cli.parse_options!
18
- cli.main
22
+ new.main
19
23
  rescue ::StandardError => e
20
- cli.handle_error(e)
24
+ warn(e.message)
21
25
  exit(1)
22
26
  end
23
27
 
24
- # Compile the given template using the given CLI flags
25
- def main
26
- parse_options! unless @options
27
- ::CSVPlusPlus.apply_template_to_sheet!(::ARGF.read, ::ARGF.filename, @options)
28
- end
29
-
30
- # Nicely handle a given error. How it's handled depends on if it's our error and if @options.verbose
31
- #
32
- # @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
33
- def handle_error(error)
34
- # make sure that we're on a newline (verbose mode might be in the middle of printing a benchmark)
35
- puts("\n\n") if @options.verbose
36
-
37
- case error
38
- when ::CSVPlusPlus::Error::Error
39
- handle_internal_error(error)
40
- when ::Google::Apis::ClientError
41
- handle_google_error(error)
42
- else
43
- unhandled_error(error)
44
- end
45
- end
28
+ sig { void }
29
+ # Initialize and parse the CLI flags provided to the program
30
+ def initialize
31
+ opts = parse_options
46
32
 
47
- # Handle the supplied command line options, setting +@options+ or throw an error if anything is invalid
48
- def parse_options!
49
- @options = ::CSVPlusPlus::Options.new
50
- option_parser.parse!
51
- validate_options
52
- rescue ::OptionParser::InvalidOption => e
53
- raise(::CSVPlusPlus::Error::Error, e.message)
33
+ @source_code = ::T.let(::CSVPlusPlus::SourceCode.new(source_code_filename), ::CSVPlusPlus::SourceCode)
34
+ @options = ::T.let(apply_options(opts), ::CSVPlusPlus::Options::Options)
54
35
  end
55
36
 
56
- # @return [::String]
57
- def to_s
58
- "CLI(options: #{options})"
37
+ sig { void }
38
+ # Compile the given template using the given CLI flags
39
+ def main
40
+ ::CSVPlusPlus.cli_compile(source_code, options)
59
41
  end
60
42
 
61
43
  private
62
44
 
63
- # An error was thrown that we weren't planning on
64
- def unhandled_error(error)
65
- warn(
66
- <<~ERROR_MESSAGE)
67
- An unexpected error was encountered. Please try running again with --verbose and
68
- reporting the error at: https://github.com/patrickomatic/csv-plus-plus/issues/new'
69
- ERROR_MESSAGE
70
-
71
- return unless @options.verbose
72
-
73
- warn(error.full_message)
74
- warn("Cause: #{error.cause}") if error.cause
75
- end
76
-
77
- def handle_internal_error(error)
78
- case error
79
- when ::CSVPlusPlus::Error::SyntaxError
80
- warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
81
- else
82
- warn(error.message)
83
- end
84
- end
85
-
86
- def handle_google_error(error)
87
- warn("Error making Google Sheets API request: #{error.message}")
88
- return unless @options.verbose
89
-
90
- warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
91
- end
92
-
93
- def validate_options
94
- error_message = @options.validate
95
- return if error_message.nil?
96
-
97
- puts(option_parser)
98
- raise(::CSVPlusPlus::Error::Error, error_message)
99
- end
100
-
45
+ sig { returns(::OptionParser) }
101
46
  def option_parser
102
47
  ::OptionParser.new do |parser|
103
48
  parser.on('-h', '--help', 'Show help information') do
@@ -105,10 +50,38 @@ module CSVPlusPlus
105
50
  exit
106
51
  end
107
52
 
108
- ::SUPPORTED_CSVPP_FLAGS.each do |f|
109
- parser.on(f.short_flag, f.long_flag, f.description) { |v| f.handler.call(@options, v) }
53
+ ::CSVPlusPlus::SUPPORTED_CSVPP_FLAGS.each do |f|
54
+ parser.on(f.short_flag, f.long_flag, f.description)
110
55
  end
111
56
  end
112
57
  end
58
+
59
+ sig { params(opts: ::T::Hash[::Symbol, ::String]).returns(::CSVPlusPlus::Options::Options) }
60
+ def apply_options(opts)
61
+ ::CSVPlusPlus::Options.from_cli_flags(opts, source_code.filename).tap do |options|
62
+ opts.each do |key, value|
63
+ ::T.must(::CSVPlusPlus::FLAG_HANDLERS[key]).call(options, value) if ::CSVPlusPlus::FLAG_HANDLERS.key?(key)
64
+ end
65
+ end
66
+ end
67
+
68
+ sig { returns(::T::Hash[::Symbol, ::String]) }
69
+ def parse_options
70
+ {}.tap do |opts|
71
+ option_parser.parse!(into: opts)
72
+ end
73
+ rescue ::OptionParser::InvalidOption => e
74
+ puts(option_parser)
75
+ raise(::CSVPlusPlus::Error::CLIError, e.message)
76
+ end
77
+
78
+ sig { returns(::String) }
79
+ # NOTE: this must be called after #parse_options, since #parse_options modifiers +ARGV+
80
+ def source_code_filename
81
+ ::ARGV.pop || raise(
82
+ ::CSVPlusPlus::Error::CLIError,
83
+ 'You must specify a source (.csvpp) file to compile as the last argument'
84
+ )
85
+ end
113
86
  end
114
87
  end