csv_plus_plus 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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