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.
- checksums.yaml +4 -4
- data/README.md +9 -5
- data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
- data/lib/csv_plus_plus/a1_reference.rb +202 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
- data/lib/csv_plus_plus/cell.rb +29 -41
- data/lib/csv_plus_plus/cli.rb +53 -80
- data/lib/csv_plus_plus/cli_flag.rb +71 -71
- data/lib/csv_plus_plus/color.rb +32 -7
- data/lib/csv_plus_plus/compiler.rb +98 -66
- data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
- data/lib/csv_plus_plus/entities/boolean.rb +26 -10
- data/lib/csv_plus_plus/entities/builtins.rb +66 -24
- data/lib/csv_plus_plus/entities/date.rb +42 -6
- data/lib/csv_plus_plus/entities/entity.rb +17 -69
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
- data/lib/csv_plus_plus/entities/function.rb +34 -11
- data/lib/csv_plus_plus/entities/function_call.rb +49 -10
- data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
- data/lib/csv_plus_plus/entities/number.rb +30 -11
- data/lib/csv_plus_plus/entities/reference.rb +77 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
- data/lib/csv_plus_plus/entities/string.rb +23 -7
- data/lib/csv_plus_plus/entities.rb +7 -16
- data/lib/csv_plus_plus/error/cli_error.rb +17 -0
- data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
- data/lib/csv_plus_plus/error/error.rb +25 -2
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
- data/lib/csv_plus_plus/error/positional_error.rb +15 -0
- data/lib/csv_plus_plus/error/writer_error.rb +8 -0
- data/lib/csv_plus_plus/error.rb +5 -1
- data/lib/csv_plus_plus/error_formatter.rb +111 -0
- data/lib/csv_plus_plus/google_api_client.rb +25 -10
- data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
- data/lib/csv_plus_plus/lexer.rb +64 -1
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
- data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
- data/lib/csv_plus_plus/modifier/expand.rb +78 -0
- data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
- data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
- data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
- data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
- data/lib/csv_plus_plus/modifier.rb +89 -160
- data/lib/csv_plus_plus/options/file_options.rb +49 -0
- data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
- data/lib/csv_plus_plus/options/options.rb +97 -0
- data/lib/csv_plus_plus/options.rb +34 -77
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
- data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
- data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
- data/lib/csv_plus_plus/reader/csv.rb +50 -0
- data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
- data/lib/csv_plus_plus/reader/reader.rb +27 -0
- data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
- data/lib/csv_plus_plus/reader.rb +14 -0
- data/lib/csv_plus_plus/row.rb +53 -12
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position.rb +242 -0
- data/lib/csv_plus_plus/runtime/references.rb +115 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
- data/lib/csv_plus_plus/runtime/scope.rb +280 -0
- data/lib/csv_plus_plus/runtime.rb +34 -191
- data/lib/csv_plus_plus/source_code.rb +71 -0
- data/lib/csv_plus_plus/template.rb +71 -39
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/csv.rb +37 -8
- data/lib/csv_plus_plus/writer/excel.rb +25 -5
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
- data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
- data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
- data/lib/csv_plus_plus/writer/merger.rb +31 -0
- data/lib/csv_plus_plus/writer/open_document.rb +21 -2
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
- data/lib/csv_plus_plus/writer/writer.rb +42 -0
- data/lib/csv_plus_plus/writer.rb +79 -10
- data/lib/csv_plus_plus.rb +47 -18
- metadata +50 -21
- data/lib/csv_plus_plus/can_define_references.rb +0 -88
- data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
- data/lib/csv_plus_plus/data_validation.rb +0 -138
- data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
- data/lib/csv_plus_plus/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
- data/lib/csv_plus_plus/expand.rb +0 -20
- data/lib/csv_plus_plus/google_options.rb +0 -27
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
- data/lib/csv_plus_plus/references.rb +0 -68
- data/lib/csv_plus_plus/scope.rb +0 -196
- data/lib/csv_plus_plus/validated_modifier.rb +0 -164
- data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4462a2a82490271a95479970e10246ed7d8b1e8a62d21928a5f2b710b2bd511
|
|
4
|
+
data.tar.gz: 31ce301945c5cc4d3154395dfa62271637baba4af78d2efed9b13d779036e015
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a968bc15a47a6ac129b0a4a13df3dfb1997991548f13af9ac28598091b5562be8d7d1943a03f86a1a501533ff2eb974d1e2cc788f43fca27f09be455edcdba9f
|
|
7
|
+
data.tar.gz: f091403bede1fb2e751fad9b6937a836f0676e5ab284690fa2c8983fa746d90e9780c701d3a7ca1161fa9aca04b84263a1b7e7789dda866d6fe1caf975593287
|
data/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+

|
|
1
2
|
[](https://rubystyle.guide)
|
|
2
3
|
[](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,
|
|
23
|
-
![[expand]],[[format=
|
|
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
|
|
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](
|
|
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
|
-
|
|
10
|
-
|
|
9
|
+
# @attr_reader benchmark [Benchmark::Report] A +Benchmark+ instance
|
|
10
|
+
class BenchmarkedCompiler < ::CSVPlusPlus::Compiler
|
|
11
|
+
extend ::T::Sig
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
data/lib/csv_plus_plus/cell.rb
CHANGED
|
@@ -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
|
-
|
|
15
|
-
attr_reader :index, :modifier
|
|
12
|
+
extend ::T::Sig
|
|
16
13
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/csv_plus_plus/cli.rb
CHANGED
|
@@ -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
|
|
5
|
+
# Handle running the application with the supported +CLIFlag+s
|
|
7
6
|
#
|
|
8
|
-
# @attr options [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
|
-
|
|
17
|
-
cli.parse_options!
|
|
18
|
-
cli.main
|
|
22
|
+
new.main
|
|
19
23
|
rescue ::StandardError => e
|
|
20
|
-
|
|
24
|
+
warn(e.message)
|
|
21
25
|
exit(1)
|
|
22
26
|
end
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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)
|
|
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
|