csv_plus_plus 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/docs/CHANGELOG.md +16 -0
  4. data/lib/csv_plus_plus/a1_reference.rb +202 -0
  5. data/lib/csv_plus_plus/benchmarked_compiler.rb +3 -3
  6. data/lib/csv_plus_plus/cell.rb +1 -35
  7. data/lib/csv_plus_plus/cli.rb +43 -80
  8. data/lib/csv_plus_plus/cli_flag.rb +71 -70
  9. data/lib/csv_plus_plus/color.rb +1 -1
  10. data/lib/csv_plus_plus/compiler.rb +31 -21
  11. data/lib/csv_plus_plus/entities/ast_builder.rb +11 -4
  12. data/lib/csv_plus_plus/entities/boolean.rb +16 -9
  13. data/lib/csv_plus_plus/entities/builtins.rb +68 -40
  14. data/lib/csv_plus_plus/entities/date.rb +14 -11
  15. data/lib/csv_plus_plus/entities/entity.rb +11 -29
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +18 -31
  17. data/lib/csv_plus_plus/entities/function.rb +22 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +35 -11
  19. data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
  20. data/lib/csv_plus_plus/entities/number.rb +15 -10
  21. data/lib/csv_plus_plus/entities/reference.rb +77 -0
  22. data/lib/csv_plus_plus/entities/runtime_value.rb +36 -23
  23. data/lib/csv_plus_plus/entities/string.rb +13 -10
  24. data/lib/csv_plus_plus/entities.rb +2 -18
  25. data/lib/csv_plus_plus/error/cli_error.rb +17 -0
  26. data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
  27. data/lib/csv_plus_plus/error/error.rb +18 -5
  28. data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -13
  29. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +10 -36
  30. data/lib/csv_plus_plus/error/modifier_validation_error.rb +6 -32
  31. data/lib/csv_plus_plus/error/positional_error.rb +15 -0
  32. data/lib/csv_plus_plus/error/writer_error.rb +1 -1
  33. data/lib/csv_plus_plus/error.rb +4 -1
  34. data/lib/csv_plus_plus/error_formatter.rb +111 -0
  35. data/lib/csv_plus_plus/google_api_client.rb +18 -8
  36. data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
  37. data/lib/csv_plus_plus/lexer/tokenizer.rb +53 -17
  38. data/lib/csv_plus_plus/lexer.rb +40 -1
  39. data/lib/csv_plus_plus/modifier/data_validation.rb +1 -1
  40. data/lib/csv_plus_plus/modifier/expand.rb +17 -0
  41. data/lib/csv_plus_plus/modifier.rb +6 -1
  42. data/lib/csv_plus_plus/options/file_options.rb +49 -0
  43. data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
  44. data/lib/csv_plus_plus/options/options.rb +97 -0
  45. data/lib/csv_plus_plus/options.rb +22 -110
  46. data/lib/csv_plus_plus/parser/cell_value.tab.rb +65 -66
  47. data/lib/csv_plus_plus/parser/code_section.tab.rb +92 -84
  48. data/lib/csv_plus_plus/parser/modifier.tab.rb +40 -30
  49. data/lib/csv_plus_plus/reader/csv.rb +50 -0
  50. data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
  51. data/lib/csv_plus_plus/reader/reader.rb +27 -0
  52. data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
  53. data/lib/csv_plus_plus/reader.rb +14 -0
  54. data/lib/csv_plus_plus/runtime/graph.rb +6 -6
  55. data/lib/csv_plus_plus/runtime/{position_tracker.rb → position.rb} +16 -5
  56. data/lib/csv_plus_plus/runtime/references.rb +32 -27
  57. data/lib/csv_plus_plus/runtime/runtime.rb +73 -67
  58. data/lib/csv_plus_plus/runtime/scope.rb +280 -0
  59. data/lib/csv_plus_plus/runtime.rb +9 -9
  60. data/lib/csv_plus_plus/source_code.rb +14 -9
  61. data/lib/csv_plus_plus/template.rb +17 -12
  62. data/lib/csv_plus_plus/version.rb +1 -1
  63. data/lib/csv_plus_plus/writer/csv.rb +32 -5
  64. data/lib/csv_plus_plus/writer/excel.rb +19 -6
  65. data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -14
  66. data/lib/csv_plus_plus/writer/google_sheets.rb +23 -129
  67. data/lib/csv_plus_plus/writer/{google_sheet_builder.rb → google_sheets_builder.rb} +39 -55
  68. data/lib/csv_plus_plus/writer/merger.rb +31 -0
  69. data/lib/csv_plus_plus/writer/open_document.rb +16 -2
  70. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +68 -43
  71. data/lib/csv_plus_plus/writer/writer.rb +42 -0
  72. data/lib/csv_plus_plus/writer.rb +58 -19
  73. data/lib/csv_plus_plus.rb +26 -14
  74. metadata +37 -12
  75. data/lib/csv_plus_plus/entities/cell_reference.rb +0 -231
  76. data/lib/csv_plus_plus/entities/variable.rb +0 -37
  77. data/lib/csv_plus_plus/error/syntax_error.rb +0 -71
  78. data/lib/csv_plus_plus/google_options.rb +0 -32
  79. data/lib/csv_plus_plus/lexer/lexer.rb +0 -89
  80. data/lib/csv_plus_plus/runtime/can_define_references.rb +0 -87
  81. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +0 -209
  82. data/lib/csv_plus_plus/writer/base_writer.rb +0 -45
@@ -2,10 +2,13 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative './error/error'
5
+ require_relative './error/positional_error'
6
+
7
+ require_relative './error/cli_error'
8
+ require_relative './error/compiler_error'
5
9
  require_relative './error/formula_syntax_error'
6
10
  require_relative './error/modifier_syntax_error'
7
11
  require_relative './error/modifier_validation_error'
8
- require_relative './error/syntax_error'
9
12
  require_relative './error/writer_error'
10
13
 
11
14
  module CSVPlusPlus
@@ -0,0 +1,111 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # Handle any errors potentially thrown during compilation. This could be anything from a user error (for example
6
+ # calling with invalid csvpp code) to an error calling Google Sheets API or writing to the filesystem.
7
+ class ErrorFormatter
8
+ extend ::T::Sig
9
+
10
+ sig do
11
+ params(options: ::CSVPlusPlus::Options::Options, runtime: ::CSVPlusPlus::Runtime::Runtime).void
12
+ end
13
+ # @param options [Options]
14
+ # @param runtime [Runtime::Runtime]
15
+ def initialize(options:, runtime:)
16
+ @options = options
17
+ @runtime = runtime
18
+ end
19
+
20
+ sig { params(error: ::StandardError).void }
21
+ # Nicely handle a given error. How it's handled depends on if it's our error and if @options.verbose
22
+ #
23
+ # @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
24
+ def handle_error(error)
25
+ # make sure that we're on a newline (verbose mode will probably be in the middle of printing a benchmark)
26
+ puts("\n\n") if @options.verbose
27
+
28
+ case error
29
+ when ::CSVPlusPlus::Error::Error
30
+ handle_internal_error(error)
31
+ when ::Google::Apis::ClientError
32
+ handle_google_error(error)
33
+ else
34
+ unhandled_error(error)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ sig { params(error: ::StandardError).void }
41
+ # An error was thrown that we weren't planning on
42
+ def unhandled_error(error)
43
+ warn(
44
+ <<~ERROR_MESSAGE)
45
+ An unexpected error was encountered. Please try running again with --verbose and
46
+ report the error at: https://github.com/patrickomatic/csv-plus-plus/issues/new'
47
+ ERROR_MESSAGE
48
+
49
+ return unless @options.verbose
50
+
51
+ warn(error.full_message)
52
+ warn("Cause: #{error.cause}") if error.cause
53
+ end
54
+
55
+ sig { params(error: ::CSVPlusPlus::Error::Error).void }
56
+ def handle_internal_error(error)
57
+ warn(with_position(error))
58
+ handle_wrapped_error(::T.must(error.wrapped_error)) if error.wrapped_error
59
+ end
60
+
61
+ sig { params(wrapped_error: ::StandardError).void }
62
+ def handle_wrapped_error(wrapped_error)
63
+ return unless @options.verbose
64
+
65
+ warn(wrapped_error.full_message)
66
+ warn((wrapped_error.backtrace || []).join("\n")) if wrapped_error.backtrace
67
+ end
68
+
69
+ sig { params(error: ::Google::Apis::ClientError).void }
70
+ def handle_google_error(error)
71
+ warn("Error making Google Sheets API request: #{error.message}")
72
+ return unless @options.verbose
73
+
74
+ warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
75
+ end
76
+
77
+ sig { params(error: ::CSVPlusPlus::Error::Error).returns(::String) }
78
+ # Output a user-helpful string that references the runtime state
79
+ #
80
+ # @param error [Error::Error] The error message to be prefixed with a filename and position
81
+ #
82
+ # @return [String]
83
+ def with_position(error)
84
+ message = error.error_message
85
+ case error
86
+ when ::CSVPlusPlus::Error::PositionalError
87
+ "#{message_prefix}#{cell_index} #{message}"
88
+ else
89
+ message
90
+ end
91
+ end
92
+
93
+ sig { returns(::String) }
94
+ def cell_index
95
+ if @runtime.parsing_csv_section?
96
+ "[#{@runtime.position.row_index},#{@runtime.position.cell_index}]"
97
+ else
98
+ ''
99
+ end
100
+ end
101
+
102
+ sig { returns(::String) }
103
+ def message_prefix
104
+ line_number = @runtime.position.line_number
105
+ filename = @runtime.source_code.filename
106
+
107
+ line_str = ":#{line_number}"
108
+ "#{filename}#{line_str}"
109
+ end
110
+ end
111
+ end
@@ -10,20 +10,30 @@ module CSVPlusPlus
10
10
  # Get a +Google::Apis::SheetsV4::SheetsService+ instance configured to connect to the sheets API
11
11
  #
12
12
  # @return [Google::Apis::SheetsV4::SheetsService]
13
- def self.sheets_client
14
- ::Google::Apis::SheetsV4::SheetsService.new.tap do |s|
15
- s.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/spreadsheets'].freeze)
16
- end
13
+ def sheets_client
14
+ ::T.must(
15
+ @sheets_client ||= ::T.let(
16
+ ::Google::Apis::SheetsV4::SheetsService.new.tap do |s|
17
+ s.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/spreadsheets'].freeze)
18
+ end,
19
+ ::T.nilable(::Google::Apis::SheetsV4::SheetsService)
20
+ )
21
+ )
17
22
  end
18
23
 
19
24
  sig { returns(::Google::Apis::DriveV3::DriveService) }
20
25
  # Get a +Google::Apis::DriveV3::DriveService+ instance connected to the drive API
21
26
  #
22
27
  # @return [Google::Apis::DriveV3::DriveService]
23
- def self.drive_client
24
- ::Google::Apis::DriveV3::DriveService.new.tap do |d|
25
- d.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/drive.file'].freeze)
26
- end
28
+ def drive_client
29
+ ::T.must(
30
+ @drive_client ||= ::T.let(
31
+ ::Google::Apis::DriveV3::DriveService.new.tap do |d|
32
+ d.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/drive.file'].freeze)
33
+ end,
34
+ ::T.nilable(::Google::Apis::DriveV3::DriveService)
35
+ )
36
+ )
27
37
  end
28
38
  end
29
39
  end
@@ -0,0 +1,144 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Lexer
6
+ # TODO: ugh clean this up
7
+ RaccToken =
8
+ ::T.type_alias do
9
+ ::T.any(
10
+ [::String, ::Symbol],
11
+ [::Symbol, ::String],
12
+ [::String, ::String],
13
+ [::Symbol, ::Symbol],
14
+ [::FalseClass, ::FalseClass]
15
+ )
16
+ end
17
+ public_constant :RaccToken
18
+
19
+ # Common methods to be mixed into the Racc parsers
20
+ #
21
+ # @attr_reader tokens [Array]
22
+ module RaccLexer
23
+ extend ::T::Sig
24
+ extend ::T::Helpers
25
+ extend ::T::Generic
26
+ include ::Kernel
27
+
28
+ abstract!
29
+
30
+ ReturnType = type_member
31
+ public_constant :ReturnType
32
+
33
+ sig { returns(::T::Array[::CSVPlusPlus::Lexer::RaccToken]) }
34
+ attr_reader :tokens
35
+
36
+ sig { params(tokens: ::T::Array[::CSVPlusPlus::Lexer::RaccToken]).void }
37
+ # Initialize a lexer instance with an empty +@tokens+
38
+ def initialize(tokens: [])
39
+ @tokens = ::T.let(tokens, ::T::Array[::CSVPlusPlus::Lexer::RaccToken])
40
+ end
41
+
42
+ sig { returns(::T.nilable(::CSVPlusPlus::Lexer::RaccToken)) }
43
+ # Used by racc to iterate each token
44
+ #
45
+ # @return [Array<(Regexp, Symbol) | (false, false)>]
46
+ def next_token
47
+ @tokens.shift
48
+ end
49
+
50
+ sig { params(input: ::String).returns(::CSVPlusPlus::Lexer::RaccLexer::ReturnType) }
51
+ # Orchestate the tokenizing, parsing and error handling of parsing input. Each instance will implement their own
52
+ # +#tokenizer+ method
53
+ #
54
+ # @return [RaccLexer#] Each instance will define it's own +return_value+ with the result of parsing
55
+ # rubocop:disable Metrics/MethodLength
56
+ def parse(input)
57
+ return return_value unless anything_to_parse?(input)
58
+
59
+ tokenize(input)
60
+ do_parse
61
+ return_value
62
+ rescue ::Racc::ParseError => e
63
+ raise(
64
+ ::CSVPlusPlus::Error::FormulaSyntaxError.new(
65
+ "Error parsing #{parse_subject}",
66
+ bad_input: e.message,
67
+ wrapped_error: e
68
+ )
69
+ )
70
+ end
71
+ # rubocop:enable Metrics/MethodLength
72
+
73
+ protected
74
+
75
+ sig { abstract.params(input: ::String).returns(::T::Boolean) }
76
+ # Is the input even worth parsing? for example we don't want to parse cells unless they're a formula (start
77
+ # with '=')
78
+ #
79
+ # @param input [String]
80
+ #
81
+ # @return [Boolean]
82
+ def anything_to_parse?(input); end
83
+
84
+ sig { abstract.returns(::String) }
85
+ # Used for error messages, what is the thing being parsed? ("cell value", "modifier", "code section")
86
+ def parse_subject; end
87
+
88
+ sig { abstract.returns(::CSVPlusPlus::Lexer::RaccLexer::ReturnType) }
89
+ # The output of the parser
90
+ def return_value; end
91
+
92
+ sig { abstract.returns(::CSVPlusPlus::Lexer::Tokenizer) }
93
+ # Returns a +Lexer::Tokenizer+ configured for the given
94
+ def tokenizer; end
95
+
96
+ private
97
+
98
+ sig { params(input: ::String).void }
99
+ def tokenize(input)
100
+ return if input.nil?
101
+
102
+ t = tokenizer.scan(input)
103
+
104
+ until t.scanner.empty?
105
+ next if t.matches_ignore?
106
+
107
+ return if t.stop?
108
+
109
+ t.scan_tokens!
110
+ consume_token(t)
111
+ end
112
+
113
+ @tokens << %i[EOL EOL]
114
+ end
115
+
116
+ sig { params(tokenizer: ::CSVPlusPlus::Lexer::Tokenizer).void }
117
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
118
+ def consume_token(tokenizer)
119
+ if tokenizer.last_token&.token && tokenizer.last_match
120
+ @tokens << [::T.must(tokenizer.last_token).token, ::T.must(tokenizer.last_match)]
121
+ elsif tokenizer.scan_catchall
122
+ @tokens << [::T.must(tokenizer.last_match), ::T.must(tokenizer.last_match)]
123
+ # TODO: checking the +parse_subject+ like this is a little hacky... but we need to know if we're parsing
124
+ # modifiers or code_section (or formulas in a cell)
125
+ elsif parse_subject == 'modifier'
126
+ raise(
127
+ ::CSVPlusPlus::Error::ModifierSyntaxError.new(
128
+ "Unable to parse #{parse_subject} starting at",
129
+ bad_input: tokenizer.peek
130
+ )
131
+ )
132
+ else
133
+ raise(
134
+ ::CSVPlusPlus::Error::FormulaSyntaxError.new(
135
+ "Unable to parse #{parse_subject} starting at",
136
+ bad_input: tokenizer.peek
137
+ )
138
+ )
139
+ end
140
+ end
141
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
142
+ end
143
+ end
144
+ end
@@ -1,24 +1,33 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'strscan'
5
-
6
4
  module CSVPlusPlus
7
5
  module Lexer
8
6
  # A class that contains the use-case-specific regexes for parsing
9
7
  #
10
- # @attr_reader last_token [String] The last token that's been matched.
11
- # @attr_reader scanner [StringScanner] The StringScanner instance that's parsing the input.
8
+ # @attr_reader last_token [String, nil] The last token that's been matched.
12
9
  class Tokenizer
13
- attr_reader :last_token, :scanner
10
+ extend ::T::Sig
11
+
12
+ sig { returns(::T.nilable(::CSVPlusPlus::Lexer::Token)) }
13
+ attr_reader :last_token
14
14
 
15
+ sig do
16
+ params(
17
+ tokens: ::T::Enumerable[::CSVPlusPlus::Lexer::Token],
18
+ catchall: ::T.nilable(::Regexp),
19
+ ignore: ::T.nilable(::Regexp),
20
+ alter_matches: ::T::Hash[::Symbol, ::T.proc.params(s: ::String).returns(::String)],
21
+ stop_fn: ::T.nilable(::T.proc.params(s: ::StringScanner).returns(::T::Boolean))
22
+ ).void
23
+ end
15
24
  # @param tokens [Array<Regexp, String>] The list of tokens to scan
16
25
  # @param catchall [Regexp] A final regexp to try if nothing else matches
17
26
  # @param ignore [Regexp] Ignore anything matching this regexp
18
27
  # @param alter_matches [Object] A map of matches to alter
19
28
  # @param stop_fn [Proc] Stop parsing when this is true
20
29
  def initialize(tokens:, catchall: nil, ignore: nil, alter_matches: {}, stop_fn: nil)
21
- @last_token = nil
30
+ @last_token = ::T.let(nil, ::T.nilable(::CSVPlusPlus::Lexer::Token))
22
31
 
23
32
  @catchall = catchall
24
33
  @ignore = ignore
@@ -27,67 +36,94 @@ module CSVPlusPlus
27
36
  @alter_matches = alter_matches
28
37
  end
29
38
 
39
+ sig { params(input: ::String).returns(::T.self_type) }
30
40
  # Initializers a scanner for the given input to be parsed
31
41
  #
32
42
  # @param input The input to be tokenized
43
+ #
33
44
  # @return [Tokenizer]
34
45
  def scan(input)
35
- @scanner = ::StringScanner.new(input.strip)
46
+ @scanner = ::T.let(::StringScanner.new(input.strip), ::T.nilable(::StringScanner))
36
47
  self
37
48
  end
38
49
 
50
+ sig { returns(::StringScanner) }
51
+ # Returns the currently initialized +StringScanner+. You must call +#scan+ first or else this will throw an
52
+ # exception.
53
+ #
54
+ # @return [StringScanner]
55
+ def scanner
56
+ # The caller needs to initialize this class with a call to #scan before we can do anything. it sets up the
57
+ # +@scanner+ with it's necessary input.
58
+ unless @scanner
59
+ raise(::CSVPlusPlus::Error::CompilerError, 'Called Tokenizer#scanner without calling #scan first')
60
+ end
61
+
62
+ @scanner
63
+ end
64
+
65
+ sig { void }
39
66
  # Scan tokens and set +@last_token+ if any match
40
67
  #
41
68
  # @return [String, nil]
42
69
  def scan_tokens!
43
- m = @tokens.find { |t| @scanner.scan(t.first) }
44
- @last_token = m ? m[1] : nil
70
+ @last_token = @tokens.find { |t| scanner.scan(t.regexp) }
45
71
  end
46
72
 
73
+ sig { returns(::T.nilable(::String)) }
47
74
  # Scan input against the catchall pattern
48
75
  #
49
76
  # @return [String, nil]
50
77
  def scan_catchall
51
- @scanner.scan(@catchall) if @catchall
78
+ scanner.scan(@catchall) if @catchall
52
79
  end
53
80
 
81
+ sig { returns(::T.nilable(::String)) }
54
82
  # Scan input against the ignore pattern
55
83
  #
56
84
  # @return [boolean]
57
85
  def matches_ignore?
58
- @scanner.scan(@ignore) if @ignore
86
+ scanner.scan(@ignore) if @ignore
59
87
  end
60
88
 
89
+ sig { returns(::T.nilable(::String)) }
61
90
  # The value of the last token matched
62
91
  #
63
92
  # @return [String, nil]
64
93
  def last_match
65
- return @alter_matches[@last_token].call(@scanner.matched) if @alter_matches.key?(@last_token)
94
+ # rubocop:disable Style/MissingElse
95
+ if @last_token && @alter_matches.key?(@last_token.token.to_sym)
96
+ # rubocop:enable Style/MissingElse
97
+ return ::T.must(@alter_matches[@last_token.token.to_sym]).call(scanner.matched)
98
+ end
66
99
 
67
- @scanner.matched
100
+ scanner.matched
68
101
  end
69
102
 
103
+ sig { params(peek_characters: ::Integer).returns(::String) }
70
104
  # Read the input but don't consume it
71
105
  #
72
106
  # @param peek_characters [Integer]
73
107
  #
74
108
  # @return [String]
75
109
  def peek(peek_characters: 100)
76
- @scanner.peek(peek_characters)
110
+ scanner.peek(peek_characters)
77
111
  end
78
112
 
113
+ sig { returns(::T::Boolean) }
79
114
  # Scan for our stop token (if there is one - some parsers stop early and some don't)
80
115
  #
81
116
  # @return [boolean]
82
117
  def stop?
83
- @stop_fn ? @stop_fn.call(@scanner) : false
118
+ @stop_fn ? @stop_fn.call(scanner) : false
84
119
  end
85
120
 
121
+ sig { returns(::String) }
86
122
  # The rest of the un-parsed input. The tokenizer might not need to parse the entire input
87
123
  #
88
124
  # @return [String]
89
125
  def rest
90
- @scanner.rest
126
+ scanner.rest
91
127
  end
92
128
  end
93
129
  end
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative './lexer/lexer'
4
+ require_relative './lexer/racc_lexer'
5
5
  require_relative './lexer/tokenizer'
6
6
 
7
7
  module CSVPlusPlus
@@ -9,12 +9,51 @@ module CSVPlusPlus
9
9
  module Lexer
10
10
  extend ::T::Sig
11
11
 
12
+ # A token that's matched by +regexp+ and presented with +token+
13
+ class Token < ::T::Struct
14
+ const :regexp, ::Regexp
15
+ const :token, ::T.any(::String, ::Symbol)
16
+ end
17
+
12
18
  END_OF_CODE_SECTION = '---'
13
19
  public_constant :END_OF_CODE_SECTION
14
20
 
15
21
  VARIABLE_REF = '$$'
16
22
  public_constant :VARIABLE_REF
17
23
 
24
+ # @see https://github.com/ruby/racc/blob/master/lib/racc/parser.rb#L121
25
+ TOKEN_LIBRARY = ::T.let(
26
+ {
27
+ # A1_NOTATION: ::CSVPlusPlus::Lexer::Token.new(
28
+ # regexp: ::CSVPlusPlus::A1Reference::A1_NOTATION_REGEXP, token: :A1_NOTATION
29
+ # ),
30
+ FALSE: ::CSVPlusPlus::Lexer::Token.new(regexp: /false/i, token: :FALSE),
31
+ HEX_COLOR: ::CSVPlusPlus::Lexer::Token.new(regexp: ::CSVPlusPlus::Color::HEX_STRING_REGEXP, token: :HEX_COLOR),
32
+ INFIX_OP: ::CSVPlusPlus::Lexer::Token.new(regexp: %r{\^|\+|-|\*|/|&|<|>|<=|>=|<>}, token: :INFIX_OP),
33
+ NUMBER: ::CSVPlusPlus::Lexer::Token.new(regexp: /-?[\d.]+/, token: :NUMBER),
34
+ REF: ::CSVPlusPlus::Lexer::Token.new(regexp: /[$!\w:]+/, token: :REF),
35
+ STRING: ::CSVPlusPlus::Lexer::Token.new(
36
+ regexp: %r{"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"},
37
+ token: :STRING
38
+ ),
39
+ TRUE: ::CSVPlusPlus::Lexer::Token.new(regexp: /true/i, token: :TRUE),
40
+ VAR_REF: ::CSVPlusPlus::Lexer::Token.new(regexp: /\$\$/, token: :VAR_REF)
41
+ }.freeze,
42
+ ::T::Hash[::Symbol, ::CSVPlusPlus::Lexer::Token]
43
+ )
44
+ public_constant :TOKEN_LIBRARY
45
+
46
+ sig { params(str: ::String).returns(::String) }
47
+ # Run any transformations to the input before going into the CSV parser
48
+ #
49
+ # The CSV parser in particular does not like if there is whitespace after a double quote and before the next comma
50
+ #
51
+ # @param str [String]
52
+ # @return [String]
53
+ def self.preprocess(str)
54
+ str.gsub(/"\s*,/, '",')
55
+ end
56
+
18
57
  sig { params(str: ::String).returns(::String) }
19
58
  # When parsing a modifier with a quoted string field, we need a way to unescape. Some examples of quoted and
20
59
  # unquoted results:
@@ -44,7 +44,7 @@ module CSVPlusPlus
44
44
  end
45
45
 
46
46
  def a1_notation(arg)
47
- return arg if ::CSVPlusPlus::Entities::CellReference.valid_cell_reference?(arg)
47
+ return arg if ::CSVPlusPlus::A1Reference.valid_cell_reference?(arg)
48
48
  end
49
49
 
50
50
  def a_date(arg, allow_relative_date: false)
@@ -56,6 +56,23 @@ module CSVPlusPlus
56
56
  @starts_at = row_index
57
57
  @ends_at = row_index + @repetitions unless @repetitions.nil?
58
58
  end
59
+
60
+ sig { params(position: ::CSVPlusPlus::Runtime::Position).returns(::T::Boolean) }
61
+ # Does the given +position+ fall within this expand?
62
+ #
63
+ # @param position [Runtime::Position]
64
+ #
65
+ # @return [boolean]
66
+ def position_within?(position)
67
+ unless starts_at
68
+ raise(
69
+ ::CSVPlusPlus::Error::CompilerError,
70
+ 'Must call Template.expand_rows! before checking the scope of expands.'
71
+ )
72
+ end
73
+
74
+ position.row_index >= ::T.must(starts_at) && (ends_at.nil? || position.row_index <= ::T.must(ends_at))
75
+ end
59
76
  end
60
77
  end
61
78
  end
@@ -76,7 +76,12 @@ module CSVPlusPlus
76
76
  end
77
77
  end
78
78
 
79
- sig { params(options: ::CSVPlusPlus::Options, row_level: ::T::Boolean).returns(::CSVPlusPlus::Modifier::Modifier) }
79
+ sig do
80
+ params(
81
+ options: ::CSVPlusPlus::Options::Options,
82
+ row_level: ::T::Boolean
83
+ ).returns(::CSVPlusPlus::Modifier::Modifier)
84
+ end
80
85
  # Return a +Modifier+ with the proper validation and helper functions attached for the given output
81
86
  #
82
87
  # @param options [boolean] is this a row level modifier? (otherwise cell-level)
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Options
6
+ # The options that are specific for compiling to a file
7
+ #
8
+ # @attr output_filename [Pathname] The file to write our compiled results to
9
+ class FileOptions < ::CSVPlusPlus::Options::Options
10
+ extend ::T::Sig
11
+ extend ::T::Helpers
12
+
13
+ sig { returns(::Pathname) }
14
+ attr_accessor :output_filename
15
+
16
+ sig { params(sheet_name: ::String, output_filename: ::String).void }
17
+ # Initialize an +Options+ object for writing to a file
18
+ def initialize(sheet_name, output_filename)
19
+ super(sheet_name)
20
+
21
+ @output_filename = ::T.let(::Pathname.new(output_filename), ::Pathname)
22
+ end
23
+
24
+ sig { override.returns(::CSVPlusPlus::Options::OutputFormat) }
25
+ # Given the options, figure out which type of +OutputFormat+ we'll be writing to
26
+ #
27
+ # @return [Options::OutputFormat]
28
+ def output_format
29
+ case output_filename.extname
30
+ when '.csv' then ::CSVPlusPlus::Options::OutputFormat::CSV
31
+ when '.ods' then ::CSVPlusPlus::Options::OutputFormat::OpenDocument
32
+ when /\.xl(sx|sm|tx|tm)$/ then ::CSVPlusPlus::Options::OutputFormat::Excel
33
+ else raise(::CSVPlusPlus::Error::CLIError, "Unsupported file extension: #{output_filename}")
34
+ end
35
+ end
36
+
37
+ sig { override.returns(::String) }
38
+ # Verbose summary for options specific to compiling to a file
39
+ #
40
+ # @return [String]
41
+ def verbose_summary
42
+ shared_summary(
43
+ <<~OUTPUT)
44
+ > Output filename | #{output_filename}
45
+ OUTPUT
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Options
6
+ # The Google-specific options a user can supply.
7
+ #
8
+ # @attr sheet_id [String] The ID of the Google Sheet to write to.
9
+ class GoogleSheetsOptions < Options
10
+ extend ::T::Sig
11
+
12
+ sig { returns(::String) }
13
+ attr_reader :sheet_id
14
+
15
+ sig { params(sheet_name: ::String, sheet_id: ::String).void }
16
+ # @param sheet_name [String] The name of the sheet
17
+ # @param sheet_id [String] The unique ID Google uses to reference the sheet
18
+ def initialize(sheet_name, sheet_id)
19
+ super(sheet_name)
20
+
21
+ @sheet_id = sheet_id
22
+ end
23
+
24
+ sig { override.returns(::CSVPlusPlus::Options::OutputFormat) }
25
+ # @return [OutputFormat]
26
+ def output_format
27
+ ::CSVPlusPlus::Options::OutputFormat::GoogleSheets
28
+ end
29
+
30
+ sig { override.returns(::String) }
31
+ # Format a string with a verbose description of Google-specific options
32
+ #
33
+ # @return [String]
34
+ def verbose_summary
35
+ shared_summary(
36
+ <<~SUMMARY)
37
+ > Sheet ID | #{@sheet_id}
38
+ SUMMARY
39
+ end
40
+ end
41
+ end
42
+ end