csv_plus_plus 0.1.1 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +18 -63
- data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
- data/lib/csv_plus_plus/cell.rb +46 -24
- data/lib/csv_plus_plus/cli.rb +44 -17
- data/lib/csv_plus_plus/cli_flag.rb +1 -2
- data/lib/csv_plus_plus/color.rb +42 -11
- data/lib/csv_plus_plus/compiler.rb +178 -0
- data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
- data/lib/csv_plus_plus/entities/boolean.rb +40 -0
- data/lib/csv_plus_plus/entities/builtins.rb +58 -0
- data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
- data/lib/csv_plus_plus/entities/date.rb +63 -0
- data/lib/csv_plus_plus/entities/entity.rb +50 -0
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
- data/lib/csv_plus_plus/entities/function.rb +45 -0
- data/lib/csv_plus_plus/entities/function_call.rb +50 -0
- data/lib/csv_plus_plus/entities/number.rb +48 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
- data/lib/csv_plus_plus/entities/string.rb +42 -0
- data/lib/csv_plus_plus/entities/variable.rb +37 -0
- data/lib/csv_plus_plus/entities.rb +40 -0
- data/lib/csv_plus_plus/error/error.rb +20 -0
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
- data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
- data/lib/csv_plus_plus/error/writer_error.rb +17 -0
- data/lib/csv_plus_plus/error.rb +10 -2
- data/lib/csv_plus_plus/google_api_client.rb +11 -2
- data/lib/csv_plus_plus/google_options.rb +23 -18
- data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
- data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
- data/lib/csv_plus_plus/lexer.rb +24 -0
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +18 -0
- data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
- data/lib/csv_plus_plus/modifier/expand.rb +61 -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 +82 -150
- data/lib/csv_plus_plus/options.rb +64 -19
- data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
- data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
- data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
- data/lib/csv_plus_plus/row.rb +53 -15
- data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
- data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
- data/lib/csv_plus_plus/runtime/references.rb +110 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
- data/lib/csv_plus_plus/runtime.rb +42 -0
- data/lib/csv_plus_plus/source_code.rb +66 -0
- data/lib/csv_plus_plus/template.rb +63 -36
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
- data/lib/csv_plus_plus/writer/csv.rb +11 -9
- data/lib/csv_plus_plus/writer/excel.rb +9 -2
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +7 -4
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
- data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
- data/lib/csv_plus_plus/writer/open_document.rb +6 -1
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -33
- data/lib/csv_plus_plus/writer.rb +39 -9
- data/lib/csv_plus_plus.rb +41 -15
- metadata +44 -30
- data/lib/csv_plus_plus/code_section.rb +0 -101
- data/lib/csv_plus_plus/expand.rb +0 -18
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
- data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
- data/lib/csv_plus_plus/language/builtins.rb +0 -46
- data/lib/csv_plus_plus/language/compiler.rb +0 -152
- data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
- data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
- data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
- data/lib/csv_plus_plus/language/entities/function.rb +0 -35
- data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
- data/lib/csv_plus_plus/language/entities/number.rb +0 -36
- data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
- data/lib/csv_plus_plus/language/entities/string.rb +0 -31
- data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/language/entities.rb +0 -28
- data/lib/csv_plus_plus/language/references.rb +0 -70
- data/lib/csv_plus_plus/language/runtime.rb +0 -205
- data/lib/csv_plus_plus/language/scope.rb +0 -192
- data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
- data/lib/csv_plus_plus/modifier.tab.rb +0 -907
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
- data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,222 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
module Modifier
|
6
|
+
# A container representing the operations that can be applied to a cell or row
|
7
|
+
#
|
8
|
+
# @attr bordercolor [Color, nil]
|
9
|
+
# @attr color [Color, nil] The background color of the cell
|
10
|
+
# @attr expand [Modifier::Expand, nil] Whether this row expands into multiple rows
|
11
|
+
# @attr fontcolor [Color, nil] The font color of the cell
|
12
|
+
# @attr fontfamily [::String, nil] The font family
|
13
|
+
# @attr fontsize [Numeric, nil] The font size
|
14
|
+
# @attr halign [Modifier::HorizontalAlign, nil] Horizontal alignment
|
15
|
+
# @attr note [::String, nil] A note/comment on the cell
|
16
|
+
# @attr numberformat [Modifier::NumberFormat, nil] A number format to apply to the value in the cell
|
17
|
+
# @attr row_level [T::Boolean] Is this a row modifier? If so it's values will apply to all cells in the row
|
18
|
+
# (unless overridden by the cell modifier)
|
19
|
+
# @attr validate [Modifier::DataValidation, nil]
|
20
|
+
# @attr valign [Modifier::VerticalAlign, nil] Vertical alignment
|
21
|
+
# @attr var [Symbol, nil] The variable bound to this cell
|
22
|
+
#
|
23
|
+
# @attr_writer borderstyle [Modifier::BorderStyle] The style of border on the cell
|
24
|
+
#
|
25
|
+
# @attr_reader borders [Set<Modifier::BorderSide>] The sides of the cell where a border will be applied.
|
26
|
+
# @attr_reader formats [Set<Modifier::TextFormat>] Bold/italics/underline and strikethrough formatting.
|
27
|
+
#
|
28
|
+
# rubocop:disable Metrics/ClassLength
|
29
|
+
class Modifier
|
30
|
+
extend ::T::Sig
|
31
|
+
|
32
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Color)) }
|
33
|
+
attr_accessor :bordercolor
|
34
|
+
|
35
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Color)) }
|
36
|
+
attr_accessor :color
|
37
|
+
|
38
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::Expand)) }
|
39
|
+
attr_accessor :expand
|
40
|
+
|
41
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Color)) }
|
42
|
+
attr_accessor :fontcolor
|
43
|
+
|
44
|
+
sig { returns(::T.nilable(::String)) }
|
45
|
+
attr_accessor :fontfamily
|
46
|
+
|
47
|
+
sig { returns(::T.nilable(::Numeric)) }
|
48
|
+
attr_accessor :fontsize
|
49
|
+
|
50
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::HorizontalAlign)) }
|
51
|
+
attr_accessor :halign
|
52
|
+
|
53
|
+
sig { returns(::T.nilable(::String)) }
|
54
|
+
attr_accessor :note
|
55
|
+
|
56
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::NumberFormat)) }
|
57
|
+
attr_accessor :numberformat
|
58
|
+
|
59
|
+
sig { returns(::T::Boolean) }
|
60
|
+
attr_accessor :row_level
|
61
|
+
|
62
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::DataValidation)) }
|
63
|
+
attr_accessor :validate
|
64
|
+
|
65
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::VerticalAlign)) }
|
66
|
+
attr_accessor :valign
|
67
|
+
|
68
|
+
sig { returns(::T.nilable(::Symbol)) }
|
69
|
+
attr_accessor :var
|
70
|
+
|
71
|
+
sig { returns(::T::Set[::CSVPlusPlus::Modifier::BorderSide]) }
|
72
|
+
attr_reader :borders
|
73
|
+
|
74
|
+
sig { returns(::T::Set[::CSVPlusPlus::Modifier::TextFormat]) }
|
75
|
+
attr_reader :formats
|
76
|
+
|
77
|
+
sig do
|
78
|
+
params(borderstyle: ::CSVPlusPlus::Modifier::BorderStyle)
|
79
|
+
.returns(::T.nilable(::CSVPlusPlus::Modifier::BorderStyle))
|
80
|
+
end
|
81
|
+
attr_writer :borderstyle
|
82
|
+
|
83
|
+
sig { params(row_level: ::T::Boolean).void }
|
84
|
+
# @param row_level [Boolean] Whether or not this modifier applies to the entire row
|
85
|
+
def initialize(row_level: false)
|
86
|
+
@row_level = row_level
|
87
|
+
@frozen = ::T.let(false, ::T::Boolean)
|
88
|
+
@borders = ::T.let(::Set.new, ::T::Set[::CSVPlusPlus::Modifier::BorderSide])
|
89
|
+
@formats = ::T.let(::Set.new, ::T::Set[::CSVPlusPlus::Modifier::TextFormat])
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { returns(::T::Boolean) }
|
93
|
+
# Are there any borders set?
|
94
|
+
#
|
95
|
+
# @return [::T::Boolean]
|
96
|
+
def any_border?
|
97
|
+
!@borders.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
sig { returns(::CSVPlusPlus::Modifier::BorderStyle) }
|
101
|
+
# Style of the border
|
102
|
+
#
|
103
|
+
# @return [::CSVPlusPlus::Modifier::BorderStyle]
|
104
|
+
def borderstyle
|
105
|
+
@borderstyle || ::CSVPlusPlus::Modifier::BorderStyle::Solid
|
106
|
+
end
|
107
|
+
|
108
|
+
sig { returns(::T::Boolean) }
|
109
|
+
# Is this a cell-level modifier?
|
110
|
+
#
|
111
|
+
# @return [T::Boolean]
|
112
|
+
def cell_level?
|
113
|
+
!@row_level
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { params(side: ::CSVPlusPlus::Modifier::BorderSide).returns(::T::Set[::CSVPlusPlus::Modifier::BorderSide]) }
|
117
|
+
# Put a border on the given +side+
|
118
|
+
#
|
119
|
+
# @param side [Modifier::BorderSide]
|
120
|
+
def border=(side)
|
121
|
+
@borders << side
|
122
|
+
end
|
123
|
+
|
124
|
+
sig { params(side: ::CSVPlusPlus::Modifier::BorderSide).returns(::T::Boolean) }
|
125
|
+
# Does this have a border along +side+?
|
126
|
+
#
|
127
|
+
# @param side [Modifier::BorderSide]
|
128
|
+
#
|
129
|
+
# @return [T::Boolean]
|
130
|
+
def border_along?(side)
|
131
|
+
@borders.include?(::CSVPlusPlus::Modifier::BorderSide::All) || @borders.include?(side)
|
132
|
+
end
|
133
|
+
|
134
|
+
sig { returns(::T::Boolean) }
|
135
|
+
# Does this have a border along all sides?
|
136
|
+
#
|
137
|
+
# @return [T::Boolean]
|
138
|
+
def border_all?
|
139
|
+
@borders.include?(::CSVPlusPlus::Modifier::BorderSide::All) \
|
140
|
+
|| (border_along?(::CSVPlusPlus::Modifier::BorderSide::Top) \
|
141
|
+
&& border_along?(::CSVPlusPlus::Modifier::BorderSide::Bottom) \
|
142
|
+
&& border_along?(::CSVPlusPlus::Modifier::BorderSide::Left) \
|
143
|
+
&& border_along?(::CSVPlusPlus::Modifier::BorderSide::Right))
|
144
|
+
end
|
145
|
+
|
146
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::Expand)) }
|
147
|
+
# Set this modifier to expand infinitely
|
148
|
+
#
|
149
|
+
# @return [Expand, nil]
|
150
|
+
def infinite_expand!
|
151
|
+
@expand = ::CSVPlusPlus::Modifier::Expand.new if row_level?
|
152
|
+
end
|
153
|
+
|
154
|
+
sig { params(format: ::CSVPlusPlus::Modifier::TextFormat).returns(::T::Set[::CSVPlusPlus::Modifier::TextFormat]) }
|
155
|
+
# Set a text format (bolid, italic, underline or strikethrough)
|
156
|
+
#
|
157
|
+
# @param format [TextFormat]
|
158
|
+
def format=(format)
|
159
|
+
@formats << format
|
160
|
+
end
|
161
|
+
|
162
|
+
sig { params(format: ::CSVPlusPlus::Modifier::TextFormat).returns(::T::Boolean) }
|
163
|
+
# Is the given format set?
|
164
|
+
#
|
165
|
+
# @param format [TextFormat]
|
166
|
+
#
|
167
|
+
# @return [T::Boolean]
|
168
|
+
def formatted?(format)
|
169
|
+
@formats.include?(format)
|
170
|
+
end
|
171
|
+
|
172
|
+
sig { returns(::T::Boolean) }
|
173
|
+
# Freeze the row or cell from edits
|
174
|
+
#
|
175
|
+
# @return [true]
|
176
|
+
def freeze!
|
177
|
+
@frozen = true
|
178
|
+
end
|
179
|
+
|
180
|
+
sig { returns(::T::Boolean) }
|
181
|
+
# Is the cell or row frozen from edits?
|
182
|
+
#
|
183
|
+
# @return [T::Boolean]
|
184
|
+
def frozen?
|
185
|
+
@frozen
|
186
|
+
end
|
187
|
+
|
188
|
+
sig { returns(::T::Boolean) }
|
189
|
+
# Mark this modifer as row-level
|
190
|
+
#
|
191
|
+
# @return [true]
|
192
|
+
def row_level!
|
193
|
+
@row_level = true
|
194
|
+
end
|
195
|
+
|
196
|
+
sig { returns(::T::Boolean) }
|
197
|
+
# Is this a row-level modifier?
|
198
|
+
#
|
199
|
+
# @return [T::Boolean]
|
200
|
+
def row_level?
|
201
|
+
@row_level
|
202
|
+
end
|
203
|
+
|
204
|
+
sig { params(other: ::CSVPlusPlus::Modifier::Modifier).void }
|
205
|
+
# Create a new modifier instance, with all values defaulted from +other+
|
206
|
+
#
|
207
|
+
# @param other [Modifier]
|
208
|
+
#
|
209
|
+
# @return [Modifier]
|
210
|
+
def take_defaults_from!(other)
|
211
|
+
other.instance_variables.each do |property|
|
212
|
+
# don't propagate row-specific values
|
213
|
+
next if property == :@row_level
|
214
|
+
|
215
|
+
value = other.instance_variable_get(property)
|
216
|
+
instance_variable_set(property, value.clone)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
# rubocop:enable Metrics/ClassLength
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
module Modifier
|
6
|
+
# Validates and coerces modifier user inputs as they are parsed.
|
7
|
+
#
|
8
|
+
# Previously this logic was handled in the parser's grammar, but with the introduction of variable binding the
|
9
|
+
# grammar is no longer context free so we need the parser to be a little looser on what it accepts and validate it
|
10
|
+
# here. Having this layer is also nice because we can provide better error messages to the user for what went
|
11
|
+
# wrong during the parse.
|
12
|
+
# rubocop:disable Metrics/ClassLength
|
13
|
+
class ModifierValidator
|
14
|
+
extend ::T::Sig
|
15
|
+
|
16
|
+
sig { returns(::CSVPlusPlus::Modifier::Modifier) }
|
17
|
+
attr_reader :modifier
|
18
|
+
|
19
|
+
sig { params(modifier: ::CSVPlusPlus::Modifier::Modifier).void }
|
20
|
+
# @param modifier [Modifier::Modifier] The modifier to set the validated attributes on.
|
21
|
+
def initialize(modifier)
|
22
|
+
@modifier = modifier
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(border_side: ::String).void }
|
26
|
+
# Validates that +border_side+ is 'all', 'top', 'bottom', 'left' or 'right'.
|
27
|
+
#
|
28
|
+
# @param border_side [::String, Modifier::BorderSide] The unvalidated user input
|
29
|
+
#
|
30
|
+
# @return [Set<Modifier::BorderSide>]
|
31
|
+
def border=(border_side)
|
32
|
+
@modifier.border = ::T.cast(
|
33
|
+
one_of(:border, border_side, ::CSVPlusPlus::Modifier::BorderSide),
|
34
|
+
::CSVPlusPlus::Modifier::BorderSide
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { params(border_color: ::String).void }
|
39
|
+
# Validates that +border_color+ is a hex color.
|
40
|
+
#
|
41
|
+
# @param border_color [::String] The unvalidated user input
|
42
|
+
def bordercolor=(border_color)
|
43
|
+
@modifier.bordercolor = color_value(:bordercolor, border_color)
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(border_style: ::String).void }
|
47
|
+
# Validates that +borderstyle+ is 'dashed', 'dotted', 'double', 'solid', 'solid_medium' or 'solid_thick'.
|
48
|
+
#
|
49
|
+
# @param border_style [::String] The unvalidated user input
|
50
|
+
def borderstyle=(border_style)
|
51
|
+
@modifier.borderstyle = ::T.cast(
|
52
|
+
one_of(:borderstyle, border_style, ::CSVPlusPlus::Modifier::BorderStyle),
|
53
|
+
::CSVPlusPlus::Modifier::BorderStyle
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { params(color: ::String).void }
|
58
|
+
# Validates that +color+ is a hex color.
|
59
|
+
#
|
60
|
+
# @param color [::String] The unvalidated user input
|
61
|
+
def color=(color)
|
62
|
+
@modifier.color = color_value(:color, color)
|
63
|
+
end
|
64
|
+
|
65
|
+
sig { params(repetitions: ::String).void }
|
66
|
+
# Validates that +repetitions+ is a positive integer.
|
67
|
+
#
|
68
|
+
# @param repetitions [String] The unvalidated user input
|
69
|
+
def expand=(repetitions)
|
70
|
+
@modifier.expand = ::CSVPlusPlus::Modifier::Expand.new(repetitions: positive_integer(:expand, repetitions))
|
71
|
+
end
|
72
|
+
|
73
|
+
sig { params(font_color: ::String).void }
|
74
|
+
# Validates that +font_color+ is a hex color.
|
75
|
+
#
|
76
|
+
# @param font_color [::String] The unvalidated user input
|
77
|
+
def fontcolor=(font_color)
|
78
|
+
@modifier.fontcolor = color_value(:fontcolor, font_color)
|
79
|
+
end
|
80
|
+
|
81
|
+
sig { params(font_family: ::String).void }
|
82
|
+
# Validates that +font_family+ is a string that looks like a valid font family. There's only so much validation
|
83
|
+
# we can do here
|
84
|
+
#
|
85
|
+
# @param font_family [::String] The unvalidated user input
|
86
|
+
def fontfamily=(font_family)
|
87
|
+
@modifier.fontfamily = matches_regexp(
|
88
|
+
:fontfamily,
|
89
|
+
::CSVPlusPlus::Lexer.unquote(font_family),
|
90
|
+
/^[\w\s]+$/,
|
91
|
+
'It is not a valid font family.'
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
sig { params(font_size: ::String).void }
|
96
|
+
# Validates that +font_size+ is a positive integer
|
97
|
+
#
|
98
|
+
# @param font_size [::String] The unvalidated user input
|
99
|
+
def fontsize=(font_size)
|
100
|
+
@modifier.fontsize = positive_integer(:fontsize, font_size)
|
101
|
+
end
|
102
|
+
|
103
|
+
sig { params(text_format: ::String).void }
|
104
|
+
# Validates that +text_format+ is 'bold', 'italic', 'strikethrough' or 'underline'.
|
105
|
+
#
|
106
|
+
# @param text_format [::String] The unvalidated user input
|
107
|
+
def format=(text_format)
|
108
|
+
@modifier.format = ::T.cast(
|
109
|
+
one_of(:format, text_format, ::CSVPlusPlus::Modifier::TextFormat),
|
110
|
+
::CSVPlusPlus::Modifier::TextFormat
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
sig { void }
|
115
|
+
# Sets the row or cell to be frozen
|
116
|
+
def freeze!
|
117
|
+
@modifier.freeze!
|
118
|
+
end
|
119
|
+
|
120
|
+
sig { void }
|
121
|
+
# Sets an infinite +Expand+ on the +Modifier+.
|
122
|
+
def infinite_expand!
|
123
|
+
@modifier.infinite_expand!
|
124
|
+
end
|
125
|
+
|
126
|
+
sig { params(halign: ::String).void }
|
127
|
+
# Validates that +halign+ is a string representation of +Modifier::HorizontalAlign+
|
128
|
+
#
|
129
|
+
# @param halign [::String] The unvalidated user input
|
130
|
+
def halign=(halign)
|
131
|
+
@modifier.halign = ::T.cast(
|
132
|
+
one_of(:halign, halign, ::CSVPlusPlus::Modifier::HorizontalAlign),
|
133
|
+
::CSVPlusPlus::Modifier::HorizontalAlign
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
sig { params(note: ::String).void }
|
138
|
+
# Validates that +note+ is a quoted string.
|
139
|
+
#
|
140
|
+
# @param note [::String] The unvalidated user input
|
141
|
+
def note=(note)
|
142
|
+
@modifier.note = note
|
143
|
+
end
|
144
|
+
|
145
|
+
sig { params(number_format: ::String).void }
|
146
|
+
# Validates that +number_format+ is a string version of a +Modifier::NumberFormat+
|
147
|
+
#
|
148
|
+
# @param number_format [::String] The unvalidated user input
|
149
|
+
def numberformat=(number_format)
|
150
|
+
@modifier.numberformat = ::T.cast(
|
151
|
+
one_of(:numberformat, number_format, ::CSVPlusPlus::Modifier::NumberFormat),
|
152
|
+
::CSVPlusPlus::Modifier::NumberFormat
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
sig { params(valign: ::String).void }
|
157
|
+
# Validates that +valign+ is a string representation of +Modifier::VerticalAlign+
|
158
|
+
#
|
159
|
+
# @param valign [::String] The unvalidated user input
|
160
|
+
def valign=(valign)
|
161
|
+
@modifier.valign = ::T.cast(
|
162
|
+
one_of(:valign, valign, ::CSVPlusPlus::Modifier::VerticalAlign),
|
163
|
+
::CSVPlusPlus::Modifier::VerticalAlign
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
sig { params(rule: ::String).void }
|
168
|
+
# Validates that the conditional validating rules are well-formed.
|
169
|
+
#
|
170
|
+
# Pretty much based off of the Google Sheets API spec here:
|
171
|
+
# @see https://developers.google.com/sheets/api/samples/data#apply_data_validation_to_a_range
|
172
|
+
#
|
173
|
+
# @param rule [::String] The validation rule to apply to this row or cell
|
174
|
+
def validate=(rule)
|
175
|
+
@modifier.validate = a_data_validation(:validate, rule)
|
176
|
+
end
|
177
|
+
|
178
|
+
sig { params(var: ::String).void }
|
179
|
+
# Validates +var+ is a valid variable identifier.
|
180
|
+
#
|
181
|
+
# @param var [::String] The unvalidated user input
|
182
|
+
def var=(var)
|
183
|
+
# TODO: I need a shared definition of what a variable can be (I guess the :ID token)
|
184
|
+
@modifier.var = matches_regexp(:var, var, /^\w+$/, 'It must be a sequence of letters, numbers and _.').to_sym
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
sig { params(modifier: ::Symbol, value: ::String).returns(::CSVPlusPlus::Modifier::DataValidation) }
|
190
|
+
def a_data_validation(modifier, value)
|
191
|
+
data_validation = ::CSVPlusPlus::Modifier::DataValidation.new(value)
|
192
|
+
return data_validation if data_validation.valid?
|
193
|
+
|
194
|
+
raise_error(modifier, value, message: data_validation.invalid_reason)
|
195
|
+
end
|
196
|
+
|
197
|
+
sig { params(modifier: ::Symbol, value: ::String).returns(::CSVPlusPlus::Color) }
|
198
|
+
def color_value(modifier, value)
|
199
|
+
unless ::CSVPlusPlus::Color.valid_hex_string?(value)
|
200
|
+
raise_error(modifier, value, message: 'It must be a 3 or 6 digit hex code.')
|
201
|
+
end
|
202
|
+
|
203
|
+
::CSVPlusPlus::Color.new(value)
|
204
|
+
end
|
205
|
+
|
206
|
+
sig { params(modifier: ::Symbol, value: ::String, regexp: ::Regexp, message: ::String).returns(::String) }
|
207
|
+
def matches_regexp(modifier, value, regexp, message)
|
208
|
+
raise_error(modifier, value, message:) unless value =~ regexp
|
209
|
+
value
|
210
|
+
end
|
211
|
+
|
212
|
+
sig { params(modifier: ::Symbol, value: ::String, choices: ::T.class_of(::T::Enum)).returns(::T::Enum) }
|
213
|
+
def one_of(modifier, value, choices)
|
214
|
+
choices.deserialize(value.downcase.gsub('_', ''))
|
215
|
+
rescue ::KeyError
|
216
|
+
raise_error(modifier, value, choices:)
|
217
|
+
end
|
218
|
+
|
219
|
+
sig { params(modifier: ::Symbol, value: ::String).returns(::Integer) }
|
220
|
+
def positive_integer(modifier, value)
|
221
|
+
Integer(value.to_s, 10).tap do |i|
|
222
|
+
raise_error(modifier, value, message: 'It must be positive and greater than 0.') unless i.positive?
|
223
|
+
end
|
224
|
+
rescue ::ArgumentError
|
225
|
+
raise_error(modifier, value, message: 'It must be a valid (whole) number.')
|
226
|
+
end
|
227
|
+
|
228
|
+
sig do
|
229
|
+
type_parameters(:E)
|
230
|
+
.params(
|
231
|
+
modifier: ::Symbol,
|
232
|
+
bad_input: ::String,
|
233
|
+
choices: ::T.nilable(::T.all(::T.type_parameter(:E), ::T.class_of(::T::Enum))),
|
234
|
+
message: ::T.nilable(::String)
|
235
|
+
).returns(::T.noreturn)
|
236
|
+
end
|
237
|
+
def raise_error(modifier, bad_input, choices: nil, message: nil)
|
238
|
+
raise(::CSVPlusPlus::Error::ModifierValidationError.new(modifier, bad_input:, choices:, message:))
|
239
|
+
end
|
240
|
+
end
|
241
|
+
# rubocop:enable Metrics/ClassLength
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
module Modifier
|
6
|
+
# Build a RubyXL-decorated Modifier class adds some support for Excel
|
7
|
+
class RubyXLModifier < ::CSVPlusPlus::Modifier::Modifier
|
8
|
+
extend ::T::Sig
|
9
|
+
|
10
|
+
# @see https://www.rubydoc.info/gems/rubyXL/RubyXL/NumberFormats
|
11
|
+
# @see https://support.microsoft.com/en-us/office/number-format-codes-5026bbd6-04bc-48cd-bf33-80f18b4eae68
|
12
|
+
NUM_FMT_IDS = ::T.let(
|
13
|
+
{
|
14
|
+
::CSVPlusPlus::Modifier::NumberFormat::Currency => 5,
|
15
|
+
::CSVPlusPlus::Modifier::NumberFormat::Date => 14,
|
16
|
+
::CSVPlusPlus::Modifier::NumberFormat::DateTime => 22,
|
17
|
+
::CSVPlusPlus::Modifier::NumberFormat::Number => 1,
|
18
|
+
::CSVPlusPlus::Modifier::NumberFormat::Percent => 9,
|
19
|
+
::CSVPlusPlus::Modifier::NumberFormat::Text => 49,
|
20
|
+
::CSVPlusPlus::Modifier::NumberFormat::Time => 21,
|
21
|
+
::CSVPlusPlus::Modifier::NumberFormat::Scientific => 48
|
22
|
+
}.freeze,
|
23
|
+
::T::Hash[::CSVPlusPlus::Modifier::NumberFormat, ::Integer]
|
24
|
+
)
|
25
|
+
private_constant :NUM_FMT_IDS
|
26
|
+
|
27
|
+
# @see http://www.datypic.com/sc/ooxml/t-ssml_ST_BorderStyle.html
|
28
|
+
# ST_BorderStyle = %w{ none thin medium dashed dotted thick double hair mediumDashed dashDot mediumDashDot
|
29
|
+
# dashDotDot slantDashDot }
|
30
|
+
BORDER_STYLES = ::T.let(
|
31
|
+
{
|
32
|
+
::CSVPlusPlus::Modifier::BorderStyle::Dashed => 'dashed',
|
33
|
+
::CSVPlusPlus::Modifier::BorderStyle::Dotted => 'dotted',
|
34
|
+
::CSVPlusPlus::Modifier::BorderStyle::Double => 'double',
|
35
|
+
::CSVPlusPlus::Modifier::BorderStyle::Solid => 'thin',
|
36
|
+
::CSVPlusPlus::Modifier::BorderStyle::SolidMedium => 'medium',
|
37
|
+
::CSVPlusPlus::Modifier::BorderStyle::SolidThick => 'thick'
|
38
|
+
}.freeze,
|
39
|
+
::T::Hash[::CSVPlusPlus::Modifier::BorderStyle, ::String]
|
40
|
+
)
|
41
|
+
private_constant :BORDER_STYLES
|
42
|
+
|
43
|
+
sig { returns(::T.nilable(::String)) }
|
44
|
+
# The excel-specific border weight
|
45
|
+
#
|
46
|
+
# @return [::String, nil]
|
47
|
+
def border_weight
|
48
|
+
# rubocop:disable Lint/ConstantResolution
|
49
|
+
BORDER_STYLES[borderstyle]
|
50
|
+
# rubocop:enable Lint/ConstantResolution
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { returns(::T.nilable(::String)) }
|
54
|
+
# The horizontal alignment, formatted for the RubyXL API
|
55
|
+
#
|
56
|
+
# @return [::String, nil]
|
57
|
+
def horizontal_alignment
|
58
|
+
@halign&.serialize
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { returns(::T.nilable(::String)) }
|
62
|
+
# The excel-specific number format code
|
63
|
+
#
|
64
|
+
# @return [::String]
|
65
|
+
def number_format_code
|
66
|
+
return unless @numberformat
|
67
|
+
|
68
|
+
::RubyXL::NumberFormats::DEFAULT_NUMBER_FORMATS.find_by_format_id(
|
69
|
+
# rubocop:disable Lint/ConstantResolution
|
70
|
+
NUM_FMT_IDS[@numberformat]
|
71
|
+
# rubocop:enable Lint/ConstantResolution
|
72
|
+
).format_code
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { returns(::T.nilable(::String)) }
|
76
|
+
# The vertical alignment, formatted for the RubyXL API
|
77
|
+
#
|
78
|
+
# @return [::String, nil]
|
79
|
+
def vertical_alignment
|
80
|
+
@valign&.serialize
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|