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.
- checksums.yaml +4 -4
- data/README.md +18 -63
- data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
- data/lib/csv_plus_plus/cell.rb +46 -24
- data/lib/csv_plus_plus/cli.rb +44 -17
- data/lib/csv_plus_plus/cli_flag.rb +1 -2
- data/lib/csv_plus_plus/color.rb +42 -11
- data/lib/csv_plus_plus/compiler.rb +178 -0
- data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
- data/lib/csv_plus_plus/entities/boolean.rb +40 -0
- data/lib/csv_plus_plus/entities/builtins.rb +58 -0
- data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
- data/lib/csv_plus_plus/entities/date.rb +63 -0
- data/lib/csv_plus_plus/entities/entity.rb +50 -0
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
- data/lib/csv_plus_plus/entities/function.rb +45 -0
- data/lib/csv_plus_plus/entities/function_call.rb +50 -0
- data/lib/csv_plus_plus/entities/number.rb +48 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
- data/lib/csv_plus_plus/entities/string.rb +42 -0
- data/lib/csv_plus_plus/entities/variable.rb +37 -0
- data/lib/csv_plus_plus/entities.rb +40 -0
- data/lib/csv_plus_plus/error/error.rb +20 -0
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
- data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
- data/lib/csv_plus_plus/error/writer_error.rb +17 -0
- data/lib/csv_plus_plus/error.rb +10 -2
- data/lib/csv_plus_plus/google_api_client.rb +11 -2
- data/lib/csv_plus_plus/google_options.rb +23 -18
- data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
- data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
- data/lib/csv_plus_plus/lexer.rb +24 -0
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +18 -0
- data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
- data/lib/csv_plus_plus/modifier/expand.rb +61 -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 +82 -150
- data/lib/csv_plus_plus/options.rb +64 -19
- data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
- data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
- data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
- data/lib/csv_plus_plus/row.rb +53 -15
- data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
- data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
- data/lib/csv_plus_plus/runtime/references.rb +110 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
- data/lib/csv_plus_plus/runtime.rb +42 -0
- data/lib/csv_plus_plus/source_code.rb +66 -0
- data/lib/csv_plus_plus/template.rb +63 -36
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
- data/lib/csv_plus_plus/writer/csv.rb +11 -9
- data/lib/csv_plus_plus/writer/excel.rb +9 -2
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +7 -4
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
- data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
- data/lib/csv_plus_plus/writer/open_document.rb +6 -1
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -33
- data/lib/csv_plus_plus/writer.rb +39 -9
- data/lib/csv_plus_plus.rb +41 -15
- metadata +44 -30
- data/lib/csv_plus_plus/code_section.rb +0 -101
- data/lib/csv_plus_plus/expand.rb +0 -18
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
- data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
- data/lib/csv_plus_plus/language/builtins.rb +0 -46
- data/lib/csv_plus_plus/language/compiler.rb +0 -152
- data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
- data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
- data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
- data/lib/csv_plus_plus/language/entities/function.rb +0 -35
- data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
- data/lib/csv_plus_plus/language/entities/number.rb +0 -36
- data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
- data/lib/csv_plus_plus/language/entities/string.rb +0 -31
- data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/language/entities.rb +0 -28
- data/lib/csv_plus_plus/language/references.rb +0 -70
- data/lib/csv_plus_plus/language/runtime.rb +0 -205
- data/lib/csv_plus_plus/language/scope.rb +0 -192
- data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
- data/lib/csv_plus_plus/modifier.tab.rb +0 -907
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
- 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
|
-
|
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
|
-
|
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
|
-
::
|
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
|
-
|
17
|
-
|
18
|
-
end
|
11
|
+
sig { returns(::String) }
|
12
|
+
attr_reader :sheet_id
|
19
13
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
|
data/lib/csv_plus_plus/lexer.rb
CHANGED
@@ -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
|