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
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