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