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
@@ -0,0 +1,133 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Modifier
6
+ # Decorate a +Modifier+ so it is more compatible with the Google Sheets API
7
+ class GoogleSheetModifier < ::CSVPlusPlus::Modifier::Modifier
8
+ extend ::T::Sig
9
+
10
+ sig { returns(::T.nilable(::Google::Apis::SheetsV4::Color)) }
11
+ # Format the color for Google Sheets
12
+ #
13
+ # @return [Google::Apis::SheetsV4::Color]
14
+ def background_color
15
+ google_sheets_color(@color) if @color
16
+ end
17
+
18
+ sig { returns(::T.nilable(::Google::Apis::SheetsV4::Border)) }
19
+ # Format the border for Google Sheets
20
+ #
21
+ # @return [Google::Apis::SheetsV4::Border]
22
+ def border
23
+ return unless any_border?
24
+
25
+ # TODO: allow different border styles per side?
26
+ ::Google::Apis::SheetsV4::Border.new(
27
+ color: google_sheets_color(bordercolor || ::CSVPlusPlus::Color.new('#000000')),
28
+ style: border_style
29
+ )
30
+ end
31
+
32
+ sig { returns(::T.nilable(::Google::Apis::SheetsV4::Color)) }
33
+ # Format the fontcolor for Google Sheets
34
+ #
35
+ # @return [Google::Apis::SheetsV4::Color]
36
+ def font_color
37
+ google_sheets_color(@fontcolor) if @fontcolor
38
+ end
39
+
40
+ sig { returns(::T.nilable(::String)) }
41
+ # Format the halign for Google Sheets
42
+ #
43
+ # @return [::String]
44
+ def horizontal_alignment
45
+ halign&.serialize&.upcase
46
+ end
47
+
48
+ sig { returns(::T.nilable(::Google::Apis::SheetsV4::NumberFormat)) }
49
+ # Format the numberformat for Google Sheets
50
+ #
51
+ #
52
+ # @return [Google::Apis::SheetsV4::NumberFormat]
53
+ def number_format
54
+ ::Google::Apis::SheetsV4::NumberFormat.new(type: number_format_type(@numberformat)) if @numberformat
55
+ end
56
+
57
+ sig { returns(::Google::Apis::SheetsV4::TextFormat) }
58
+ # Builds a SheetsV4::TextFormat with the underlying Modifier
59
+ #
60
+ # @return [Google::Apis::SheetsV4::TextFormat]
61
+ def text_format
62
+ ::Google::Apis::SheetsV4::TextFormat.new(
63
+ bold: formatted?(::CSVPlusPlus::Modifier::TextFormat::Bold) || nil,
64
+ italic: formatted?(::CSVPlusPlus::Modifier::TextFormat::Italic) || nil,
65
+ strikethrough: formatted?(::CSVPlusPlus::Modifier::TextFormat::Strikethrough) || nil,
66
+ underline: formatted?(::CSVPlusPlus::Modifier::TextFormat::Underline) || nil,
67
+ font_family: fontfamily,
68
+ font_size: fontsize,
69
+ foreground_color: font_color
70
+ )
71
+ end
72
+
73
+ sig { returns(::T.nilable(::String)) }
74
+ # Format the valign for Google Sheets
75
+ def vertical_alignment
76
+ valign&.serialize&.upcase
77
+ end
78
+
79
+ private
80
+
81
+ sig { returns(::T.nilable(::String)) }
82
+ # Format the border style for Google Sheets
83
+ #
84
+ # @see https://developers.google.com/apps-script/reference/spreadsheet/border-style
85
+ #
86
+ # @return [::String, nil]
87
+ # rubocop:disable Metrics/CyclomaticComplexity
88
+ def border_style
89
+ return 'SOLID' unless @borderstyle
90
+
91
+ case @borderstyle
92
+ when ::CSVPlusPlus::Modifier::BorderStyle::Dashed then 'DASHED'
93
+ when ::CSVPlusPlus::Modifier::BorderStyle::Dotted then 'DOTTED'
94
+ when ::CSVPlusPlus::Modifier::BorderStyle::Double then 'DOUBLE'
95
+ when ::CSVPlusPlus::Modifier::BorderStyle::Solid then 'SOLID'
96
+ when ::CSVPlusPlus::Modifier::BorderStyle::SolidMedium then 'SOLID_MEDIUM'
97
+ when ::CSVPlusPlus::Modifier::BorderStyle::SolidThick then 'SOLID_THICK'
98
+ else ::T.absurd(@borderstyle)
99
+ end
100
+ end
101
+ # rubocop:enable Metrics/CyclomaticComplexity
102
+
103
+ sig { params(color: ::CSVPlusPlus::Color).returns(::Google::Apis::SheetsV4::Color) }
104
+ def google_sheets_color(color)
105
+ ::Google::Apis::SheetsV4::Color.new(
106
+ red: color.red_percent,
107
+ green: color.green_percent,
108
+ blue: color.blue_percent
109
+ )
110
+ end
111
+
112
+ sig { params(numberformat: ::CSVPlusPlus::Modifier::NumberFormat).returns(::String) }
113
+ # @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#NumberFormat
114
+ #
115
+ # @return [::String]
116
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
117
+ def number_format_type(numberformat)
118
+ case numberformat
119
+ when ::CSVPlusPlus::Modifier::NumberFormat::Currency then 'CURRENCY'
120
+ when ::CSVPlusPlus::Modifier::NumberFormat::Date then 'DATE'
121
+ when ::CSVPlusPlus::Modifier::NumberFormat::DateTime then 'DATE_TIME'
122
+ when ::CSVPlusPlus::Modifier::NumberFormat::Number then 'NUMBER'
123
+ when ::CSVPlusPlus::Modifier::NumberFormat::Percent then 'PERCENT'
124
+ when ::CSVPlusPlus::Modifier::NumberFormat::Text then 'TEXT'
125
+ when ::CSVPlusPlus::Modifier::NumberFormat::Time then 'TIME'
126
+ when ::CSVPlusPlus::Modifier::NumberFormat::Scientific then 'SCIENTIFIC'
127
+ else ::T.absurd(numberformat)
128
+ end
129
+ end
130
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
131
+ end
132
+ end
133
+ end
@@ -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