csv_plus_plus 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -5
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  6. data/lib/csv_plus_plus/cell.rb +29 -41
  7. data/lib/csv_plus_plus/cli.rb +53 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +71 -71
  9. data/lib/csv_plus_plus/color.rb +32 -7
  10. data/lib/csv_plus_plus/compiler.rb +98 -66
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
  12. data/lib/csv_plus_plus/entities/boolean.rb +26 -10
  13. data/lib/csv_plus_plus/entities/builtins.rb +66 -24
  14. data/lib/csv_plus_plus/entities/date.rb +42 -6
  15. data/lib/csv_plus_plus/entities/entity.rb +17 -69
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
  17. data/lib/csv_plus_plus/entities/function.rb +34 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +49 -10
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +30 -11
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
  23. data/lib/csv_plus_plus/entities/string.rb +23 -7
  24. data/lib/csv_plus_plus/entities.rb +7 -16
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +25 -2
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  33. data/lib/csv_plus_plus/error.rb +5 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +25 -10
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
  38. data/lib/csv_plus_plus/lexer.rb +64 -1
  39. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  40. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  41. data/lib/csv_plus_plus/modifier/expand.rb +78 -0
  42. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  43. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  44. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  45. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  46. data/lib/csv_plus_plus/modifier.rb +89 -160
  47. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  48. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  49. data/lib/csv_plus_plus/options/options.rb +97 -0
  50. data/lib/csv_plus_plus/options.rb +34 -77
  51. data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
  52. data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
  53. data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
  54. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  55. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  56. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  57. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  58. data/lib/csv_plus_plus/reader.rb +14 -0
  59. data/lib/csv_plus_plus/row.rb +53 -12
  60. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  61. data/lib/csv_plus_plus/runtime/position.rb +242 -0
  62. data/lib/csv_plus_plus/runtime/references.rb +115 -0
  63. data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
  64. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  65. data/lib/csv_plus_plus/runtime.rb +34 -191
  66. data/lib/csv_plus_plus/source_code.rb +71 -0
  67. data/lib/csv_plus_plus/template.rb +71 -39
  68. data/lib/csv_plus_plus/version.rb +2 -1
  69. data/lib/csv_plus_plus/writer/csv.rb +37 -8
  70. data/lib/csv_plus_plus/writer/excel.rb +25 -5
  71. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
  72. data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
  73. data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
  74. data/lib/csv_plus_plus/writer/merger.rb +31 -0
  75. data/lib/csv_plus_plus/writer/open_document.rb +21 -2
  76. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
  77. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  78. data/lib/csv_plus_plus/writer.rb +79 -10
  79. data/lib/csv_plus_plus.rb +47 -18
  80. metadata +50 -21
  81. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  82. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  83. data/lib/csv_plus_plus/data_validation.rb +0 -138
  84. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
  85. data/lib/csv_plus_plus/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
  87. data/lib/csv_plus_plus/expand.rb +0 -20
  88. data/lib/csv_plus_plus/google_options.rb +0 -27
  89. data/lib/csv_plus_plus/graph.rb +0 -62
  90. data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
  91. data/lib/csv_plus_plus/references.rb +0 -68
  92. data/lib/csv_plus_plus/scope.rb +0 -196
  93. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  94. data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
  95. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
  96. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  97. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -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