csv_plus_plus 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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