csv_plus_plus 0.1.1 → 0.1.3

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -63
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +44 -17
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +42 -11
  9. data/lib/csv_plus_plus/compiler.rb +178 -0
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
  11. data/lib/csv_plus_plus/entities/boolean.rb +40 -0
  12. data/lib/csv_plus_plus/entities/builtins.rb +58 -0
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
  14. data/lib/csv_plus_plus/entities/date.rb +63 -0
  15. data/lib/csv_plus_plus/entities/entity.rb +50 -0
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +45 -0
  18. data/lib/csv_plus_plus/entities/function_call.rb +50 -0
  19. data/lib/csv_plus_plus/entities/number.rb +48 -0
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
  21. data/lib/csv_plus_plus/entities/string.rb +42 -0
  22. data/lib/csv_plus_plus/entities/variable.rb +37 -0
  23. data/lib/csv_plus_plus/entities.rb +40 -0
  24. data/lib/csv_plus_plus/error/error.rb +20 -0
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
  28. data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
  29. data/lib/csv_plus_plus/error/writer_error.rb +17 -0
  30. data/lib/csv_plus_plus/error.rb +10 -2
  31. data/lib/csv_plus_plus/google_api_client.rb +11 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
  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 +18 -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 -150
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
  46. data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
  48. data/lib/csv_plus_plus/row.rb +53 -15
  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 +42 -0
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +63 -36
  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 +7 -4
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
  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 -33
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +41 -15
  69. metadata +44 -30
  70. data/lib/csv_plus_plus/code_section.rb +0 -101
  71. data/lib/csv_plus_plus/expand.rb +0 -18
  72. data/lib/csv_plus_plus/graph.rb +0 -62
  73. data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
  74. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  75. data/lib/csv_plus_plus/language/builtins.rb +0 -46
  76. data/lib/csv_plus_plus/language/compiler.rb +0 -152
  77. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  78. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  79. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  80. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  81. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
  82. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  83. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  84. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  85. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/language/entities.rb +0 -28
  87. data/lib/csv_plus_plus/language/references.rb +0 -70
  88. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  89. data/lib/csv_plus_plus/language/scope.rb +0 -192
  90. data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
  91. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
  92. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
  93. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -1,16 +1,25 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
5
  # A convenience wrapper around Google's REST API client
5
6
  module GoogleApiClient
6
- # Get a +::Google::Apis::SheetsV4::SheetsService+ instance connected to the sheets API
7
+ extend ::T::Sig
8
+
9
+ sig { returns(::Google::Apis::SheetsV4::SheetsService) }
10
+ # Get a +Google::Apis::SheetsV4::SheetsService+ instance configured to connect to the sheets API
11
+ #
12
+ # @return [Google::Apis::SheetsV4::SheetsService]
7
13
  def self.sheets_client
8
14
  ::Google::Apis::SheetsV4::SheetsService.new.tap do |s|
9
15
  s.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/spreadsheets'].freeze)
10
16
  end
11
17
  end
12
18
 
13
- # Get a +::Google::Apis::DriveV3::DriveService+ instance connected to the drive API
19
+ sig { returns(::Google::Apis::DriveV3::DriveService) }
20
+ # Get a +Google::Apis::DriveV3::DriveService+ instance connected to the drive API
21
+ #
22
+ # @return [Google::Apis::DriveV3::DriveService]
14
23
  def self.drive_client
15
24
  ::Google::Apis::DriveV3::DriveService.new.tap do |d|
16
25
  d.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/drive.file'].freeze)
@@ -1,27 +1,32 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
4
- # The Google-specific options a user can supply
5
+ # The Google-specific options a user can supply.
5
6
  #
6
- # attr sheet_id [String] The ID of the Google Sheet to write to
7
- GoogleOptions =
8
- ::Struct.new(:sheet_id) do
9
- # Format a string with a verbose description of what we're doing with the options
10
- #
11
- # @return [String]
12
- def verbose_summary
13
- <<~SUMMARY
14
- ## Google Sheets Options
7
+ # @attr sheet_id [String] The ID of the Google Sheet to write to.
8
+ class GoogleOptions
9
+ extend ::T::Sig
15
10
 
16
- > Sheet ID | #{sheet_id}
17
- SUMMARY
18
- end
11
+ sig { returns(::String) }
12
+ attr_reader :sheet_id
19
13
 
20
- # @return [String]
21
- def to_s
22
- "GoogleOptions(sheet_id: #{sheet_id})"
23
- end
14
+ sig { params(sheet_id: ::String).void }
15
+ # @param sheet_id [String] The unique ID Google uses to reference the sheet
16
+ def initialize(sheet_id)
17
+ @sheet_id = sheet_id
24
18
  end
25
19
 
26
- public_constant :GoogleOptions
20
+ sig { returns(::String) }
21
+ # Format a string with a verbose description of what we're doing with the options
22
+ #
23
+ # @return [String]
24
+ def verbose_summary
25
+ <<~SUMMARY
26
+ ## Google Sheets Options
27
+
28
+ > Sheet ID | #{@sheet_id}
29
+ SUMMARY
30
+ end
31
+ end
27
32
  end
@@ -1,3 +1,4 @@
1
+ # typed: false
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -28,21 +29,27 @@ module CSVPlusPlus
28
29
 
29
30
  return return_value unless anything_to_parse?(input)
30
31
 
32
+ @runtime = runtime
33
+
31
34
  tokenize(input, runtime)
32
35
  do_parse
33
36
  return_value
34
37
  rescue ::Racc::ParseError => e
35
- runtime.raise_syntax_error("Error parsing #{parse_subject}", e.message, wrapped_error: e)
38
+ runtime.raise_formula_syntax_error("Error parsing #{parse_subject}", e.message, wrapped_error: e)
39
+ rescue ::CSVPlusPlus::Error::ModifierValidationError => e
40
+ raise(::CSVPlusPlus::Error::ModifierSyntaxError.from_validation_error(runtime, e))
36
41
  end
37
42
 
38
43
  TOKEN_LIBRARY = {
39
- TRUE: [/true/i, :TRUE],
44
+ A1_NOTATION: [::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP, :A1_NOTATION],
40
45
  FALSE: [/false/i, :FALSE],
46
+ HEX_COLOR: [::CSVPlusPlus::Color::HEX_STRING_REGEXP, :HEX_COLOR],
47
+ ID: [/[$!\w:]+/, :ID],
48
+ INFIX_OP: [%r{\^|\+|-|\*|/|&|<|>|<=|>=|<>}, :INFIX_OP],
41
49
  NUMBER: [/-?[\d.]+/, :NUMBER],
42
50
  STRING: [%r{"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"}, :STRING],
43
- INFIX_OP: [%r{\^|\+|-|\*|/|&|<|>|<=|>=|<>}, :INFIX_OP],
44
- VAR_REF: [/\$\$/, :VAR_REF],
45
- ID: [/[$!\w:]+/, :ID]
51
+ TRUE: [/true/i, :TRUE],
52
+ VAR_REF: [/\$\$/, :VAR_REF]
46
53
  }.freeze
47
54
  public_constant :TOKEN_LIBRARY
48
55
 
@@ -70,8 +77,12 @@ module CSVPlusPlus
70
77
  @tokens << [tokenizer.last_token, tokenizer.last_match]
71
78
  elsif tokenizer.scan_catchall
72
79
  @tokens << [tokenizer.last_match, tokenizer.last_match]
80
+ # TODO: checking the +parse_subject+ like this is a little hacky... but we need to know if we're parsing
81
+ # modifiers or code_section (or formulas in a cell)
82
+ elsif parse_subject == 'modifier'
83
+ runtime.raise_modifier_syntax_error("Unable to parse #{parse_subject} starting at", tokenizer.peek)
73
84
  else
74
- runtime.raise_syntax_error("Unable to parse #{parse_subject} starting at", tokenizer.peek)
85
+ runtime.raise_formula_syntax_error("Unable to parse #{parse_subject} starting at", tokenizer.peek)
75
86
  end
76
87
  end
77
88
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'strscan'
@@ -11,7 +12,11 @@ module CSVPlusPlus
11
12
  class Tokenizer
12
13
  attr_reader :last_token, :scanner
13
14
 
14
- # @param input [String]
15
+ # @param tokens [Array<Regexp, String>] The list of tokens to scan
16
+ # @param catchall [Regexp] A final regexp to try if nothing else matches
17
+ # @param ignore [Regexp] Ignore anything matching this regexp
18
+ # @param alter_matches [Object] A map of matches to alter
19
+ # @param stop_fn [Proc] Stop parsing when this is true
15
20
  def initialize(tokens:, catchall: nil, ignore: nil, alter_matches: {}, stop_fn: nil)
16
21
  @last_token = nil
17
22
 
@@ -1,14 +1,38 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative './lexer/lexer'
4
5
  require_relative './lexer/tokenizer'
5
6
 
6
7
  module CSVPlusPlus
8
+ # Code for tokenizing a csvpp file
7
9
  module Lexer
10
+ extend ::T::Sig
11
+
8
12
  END_OF_CODE_SECTION = '---'
9
13
  public_constant :END_OF_CODE_SECTION
10
14
 
11
15
  VARIABLE_REF = '$$'
12
16
  public_constant :VARIABLE_REF
17
+
18
+ sig { params(str: ::String).returns(::String) }
19
+ # When parsing a modifier with a quoted string field, we need a way to unescape. Some examples of quoted and
20
+ # unquoted results:
21
+ #
22
+ # * "just a string" => "just a string"
23
+ # * "' this is a string'" => "this is a string"
24
+ # * "won\'t this work?" => "won't this work"
25
+ #
26
+ # @param str [::String]
27
+ #
28
+ # @return [::String]
29
+ def self.unquote(str)
30
+ # could probably do this with one regex but we do it in 3 steps:
31
+ #
32
+ # 1. remove leading and trailing spaces and '
33
+ # 2. remove any backslashes that are by themselves (none on either side)
34
+ # 3. turn double backslashes into singles
35
+ str.gsub(/^\s*'?|'?\s*$/, '').gsub(/([^\\]+)\\([^\\]+)/, '\1\2').gsub(/\\\\/, '\\')
36
+ end
13
37
  end
14
38
  end
@@ -0,0 +1,18 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Modifier
6
+ # A class that handles the rules for modifiers to support conditional formatting.
7
+ class ConditionalFormatting
8
+ attr_reader :arguments, :condition, :invalid_reason
9
+
10
+ # @param value [::String] The unparsed conditional formatting rule
11
+ def initialize(value)
12
+ condition, args = value.split(/\si:\s*/)
13
+ @condition = condition.to_sym
14
+ @arguments = args.split(/\s+/)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -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