csv_plus_plus 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +9 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +23 -13
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +32 -7
  9. data/lib/csv_plus_plus/compiler.rb +82 -60
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +27 -43
  11. data/lib/csv_plus_plus/entities/boolean.rb +18 -9
  12. data/lib/csv_plus_plus/entities/builtins.rb +23 -9
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +200 -29
  14. data/lib/csv_plus_plus/entities/date.rb +38 -5
  15. data/lib/csv_plus_plus/entities/entity.rb +27 -61
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +23 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +24 -9
  19. data/lib/csv_plus_plus/entities/number.rb +24 -10
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +22 -5
  21. data/lib/csv_plus_plus/entities/string.rb +19 -6
  22. data/lib/csv_plus_plus/entities/variable.rb +16 -4
  23. data/lib/csv_plus_plus/entities.rb +20 -13
  24. data/lib/csv_plus_plus/error/error.rb +11 -1
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +1 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +53 -5
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +34 -14
  28. data/lib/csv_plus_plus/error/syntax_error.rb +22 -9
  29. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  30. data/lib/csv_plus_plus/error.rb +1 -0
  31. data/lib/csv_plus_plus/google_api_client.rb +7 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +8 -4
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -158
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/parser/cell_value.tab.rb +5 -5
  46. data/lib/csv_plus_plus/parser/code_section.tab.rb +8 -13
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +17 -23
  48. data/lib/csv_plus_plus/row.rb +53 -12
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +34 -191
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +62 -35
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +1 -0
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +71 -23
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -30
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +29 -12
  69. metadata +18 -14
  70. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  71. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  72. data/lib/csv_plus_plus/data_validation.rb +0 -138
  73. data/lib/csv_plus_plus/expand.rb +0 -20
  74. data/lib/csv_plus_plus/graph.rb +0 -62
  75. data/lib/csv_plus_plus/references.rb +0 -68
  76. data/lib/csv_plus_plus/scope.rb +0 -196
  77. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  78. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  79. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,138 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Modifier
6
+ # A validation on a cell value. Used to support the `validate=` modifier directive. This is mostly based on the
7
+ # Google Sheets API spec:
8
+ #
9
+ # @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType
10
+ #
11
+ # @attr_reader arguments [Array<::String>] The parsed arguments as required by the condition.
12
+ # @attr_reader condition [Symbol] The condition (:blank, :text_eq, :date_before, etc.)
13
+ # @attr_reader invalid_reason [::String, nil] If set, the reason why this modifier is not valid.
14
+ class DataValidation
15
+ attr_reader :arguments, :condition, :invalid_reason
16
+
17
+ # @param value [::String] The value to parse as a data validation
18
+ def initialize(value)
19
+ condition, args = value.split(/\s*:\s*/)
20
+ @arguments = ::CSVPlusPlus::Lexer.unquote(args || '').split(/\s+/)
21
+ @condition = condition.to_sym
22
+
23
+ validate!
24
+ end
25
+
26
+ # Each data validation (represented by +@condition+) has their own requirements for which arguments are valid.
27
+ # If this object is invalid, you can see the reason in +@invalid_reason+.
28
+ #
29
+ # @return [boolean]
30
+ def valid?
31
+ @invalid_reason.nil?
32
+ end
33
+
34
+ protected
35
+
36
+ def invalid!(reason)
37
+ @invalid_reason = reason
38
+ end
39
+
40
+ def a_number(arg)
41
+ Float(arg)
42
+ rescue ::ArgumentError
43
+ invalid!("Requires a number but given: #{arg}")
44
+ end
45
+
46
+ def a1_notation(arg)
47
+ return arg if ::CSVPlusPlus::Entities::CellReference.valid_cell_reference?(arg)
48
+ end
49
+
50
+ def a_date(arg, allow_relative_date: false)
51
+ return arg if ::CSVPlusPlus::Entities::Date.valid_date?(arg)
52
+
53
+ if allow_relative_date
54
+ a_relative_date(arg)
55
+ else
56
+ invalid!("Requires a date but given: #{arg}")
57
+ end
58
+ end
59
+
60
+ def a_relative_date(arg)
61
+ return arg if %w[past_month past_week past_year yesterday today tomorrow].include?(arg.downcase)
62
+
63
+ invalid!('Requires a relative date: past_month, past_week, past_year, yesterday, today or tomorrow')
64
+ end
65
+
66
+ def no_args
67
+ return if @arguments.empty?
68
+
69
+ invalid!("Requires no arguments but #{@arguments.length} given: #{@arguments}")
70
+ end
71
+
72
+ def one_arg
73
+ return @arguments[0] if @arguments.length == 1
74
+
75
+ invalid!("Requires only one argument but #{@arguments.length} given: #{@arguments}")
76
+ end
77
+
78
+ def one_arg_or_more
79
+ return @arguments if @arguments.length.positive?
80
+
81
+ invalid!("Requires at least one argument but #{@arguments.length} given: #{@arguments}")
82
+ end
83
+
84
+ def two_dates
85
+ return @arguments if @arguments.length == 2 && a_date(@arguments[0]) && a_date(@arguments[1])
86
+
87
+ invalid!("Requires exactly two dates but given: #{@arguments}")
88
+ end
89
+
90
+ def two_numbers
91
+ return @arguments if @arguments.length == 2 && a_number(@arguments[0]) && a_number(@arguments[1])
92
+
93
+ invalid!("Requires exactly two numbers but given: #{@arguments}")
94
+ end
95
+
96
+ # validate_boolean is a weird one because it can have 0, 1 or 2 @arguments - all of them must be (true | false)
97
+ def validate_boolean
98
+ return @arguments if @arguments.empty?
99
+
100
+ converted_args = @arguments.map(&:strip).map(&:downcase)
101
+ return @arguments if [1, 2].include?(@arguments.length) && converted_args.all? do |arg|
102
+ %w[true false].include?(arg)
103
+ end
104
+
105
+ invalid!("Requires 0, 1 or 2 arguments and they all must be either 'true' or 'false'. Received: #{arguments}")
106
+ end
107
+
108
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
109
+ def validate!
110
+ case condition.to_sym
111
+ when :blank, :date_is_valid, :not_blank, :text_is_email, :text_is_url
112
+ no_args
113
+ when :text_contains, :text_ends_with, :text_eq, :text_not_contains, :text_starts_with
114
+ one_arg
115
+ when :date_after, :date_before, :date_on_or_after, :date_on_or_before
116
+ a_date(one_arg, allow_relative_date: true)
117
+ when :date_eq, :date_not_eq
118
+ a_date(one_arg)
119
+ when :date_between, :date_not_between
120
+ two_dates
121
+ when :one_of_range
122
+ a1_notation(one_arg)
123
+ when :custom_formula, :one_of_list, :text_not_eq
124
+ one_arg_or_more
125
+ when :number_eq, :number_greater, :number_greater_than_eq, :number_less, :number_less_than_eq, :number_not_eq
126
+ a_number(one_arg)
127
+ when :number_between, :number_not_between
128
+ two_numbers
129
+ when :boolean
130
+ validate_boolean
131
+ else
132
+ invalid!('Not a recognized data validation directive')
133
+ end
134
+ end
135
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,61 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Modifier
6
+ # The logic for how a row can expand
7
+ #
8
+ # @attr_reader ends_at [Integer, nil] Once the row has been expanded, where it ends at.
9
+ # @attr_reader repetitions [Integer, nil] How many times the row repeats/expands.
10
+ # @attr_reader starts_at [Integer, nil] Once the row has been expanded, where it starts at.
11
+ class Expand
12
+ extend ::T::Sig
13
+
14
+ sig { returns(::T.nilable(::Integer)) }
15
+ attr_reader :ends_at
16
+
17
+ sig { returns(::T.nilable(::Integer)) }
18
+ attr_reader :repetitions
19
+
20
+ sig { returns(::T.nilable(::Integer)) }
21
+ attr_reader :starts_at
22
+
23
+ sig { params(repetitions: ::T.nilable(::Integer), starts_at: ::T.nilable(::Integer)).void }
24
+ # @param repetitions [Integer, nil] How many times this expand repeats. If it's +nil+ it will expand infinitely
25
+ # (for the rest of the worksheet.)
26
+ # @param starts_at [Integer, nil] The final location where the +Expand+ will start. It's important to note that
27
+ # this can't be derived until all rows are expanded, because each expand modifier will push down the ones below
28
+ # it. So typically this param will not be passed in the initializer but instead set later.
29
+ def initialize(repetitions: nil, starts_at: nil)
30
+ @repetitions = ::T.let(repetitions, ::T.nilable(::Integer))
31
+ @starts_at = ::T.let(starts_at, ::T.nilable(::Integer)) unless starts_at.nil?
32
+ @ends_at = ::T.let(nil, ::T.nilable(::Integer))
33
+ end
34
+
35
+ sig { returns(::T::Boolean) }
36
+ # Has the row been expanded?
37
+ #
38
+ # @return [boolean]
39
+ def expanded?
40
+ !@starts_at.nil?
41
+ end
42
+
43
+ sig { returns(::T::Boolean) }
44
+ # Does this infinitely expand?
45
+ #
46
+ # @return [boolean]
47
+ def infinite?
48
+ repetitions.nil?
49
+ end
50
+
51
+ sig { params(row_index: ::Integer).void }
52
+ # Mark the start of the row once it's been expanded, as well as where it +ends_at+. When expanding rows each one
53
+ # adds rows to the worksheet and if there are multiple `expand` modifiers in the worksheet, we don't know the
54
+ # final +row_index+ until we're in the phase of expanding all the rows out.
55
+ def starts_at=(row_index)
56
+ @starts_at = row_index
57
+ @ends_at = row_index + @repetitions unless @repetitions.nil?
58
+ end
59
+ end
60
+ end
61
+ end
@@ -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