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