csv_plus_plus 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -63
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +44 -17
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +42 -11
  9. data/lib/csv_plus_plus/compiler.rb +178 -0
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
  11. data/lib/csv_plus_plus/entities/boolean.rb +40 -0
  12. data/lib/csv_plus_plus/entities/builtins.rb +58 -0
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
  14. data/lib/csv_plus_plus/entities/date.rb +63 -0
  15. data/lib/csv_plus_plus/entities/entity.rb +50 -0
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +45 -0
  18. data/lib/csv_plus_plus/entities/function_call.rb +50 -0
  19. data/lib/csv_plus_plus/entities/number.rb +48 -0
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
  21. data/lib/csv_plus_plus/entities/string.rb +42 -0
  22. data/lib/csv_plus_plus/entities/variable.rb +37 -0
  23. data/lib/csv_plus_plus/entities.rb +40 -0
  24. data/lib/csv_plus_plus/error/error.rb +20 -0
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
  28. data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
  29. data/lib/csv_plus_plus/error/writer_error.rb +17 -0
  30. data/lib/csv_plus_plus/error.rb +10 -2
  31. data/lib/csv_plus_plus/google_api_client.rb +11 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +18 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -150
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
  46. data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
  48. data/lib/csv_plus_plus/row.rb +53 -15
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +42 -0
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +63 -36
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +7 -4
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -33
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +41 -15
  69. metadata +44 -30
  70. data/lib/csv_plus_plus/code_section.rb +0 -101
  71. data/lib/csv_plus_plus/expand.rb +0 -18
  72. data/lib/csv_plus_plus/graph.rb +0 -62
  73. data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
  74. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  75. data/lib/csv_plus_plus/language/builtins.rb +0 -46
  76. data/lib/csv_plus_plus/language/compiler.rb +0 -152
  77. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  78. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  79. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  80. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  81. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
  82. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  83. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  84. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  85. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/language/entities.rb +0 -28
  87. data/lib/csv_plus_plus/language/references.rb +0 -70
  88. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  89. data/lib/csv_plus_plus/language/scope.rb +0 -192
  90. data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
  91. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
  92. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
  93. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,178 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+). Variable resolution is delegated
6
+ # to the +Scope+
7
+ #
8
+ # @attr_reader options [Options] The +Options+ to compile with
9
+ # @attr_reader runtime [Runtime] The runtime execution
10
+ # rubocop:disable Metrics/ClassLength
11
+ class Compiler
12
+ extend ::T::Sig
13
+
14
+ sig { returns(::CSVPlusPlus::Options) }
15
+ attr_reader :options
16
+
17
+ sig { returns(::CSVPlusPlus::Runtime::Runtime) }
18
+ attr_reader :runtime
19
+
20
+ sig do
21
+ params(
22
+ options: ::CSVPlusPlus::Options,
23
+ runtime: ::CSVPlusPlus::Runtime::Runtime,
24
+ block: ::T.proc.params(arg0: ::CSVPlusPlus::Compiler).void
25
+ ).void
26
+ end
27
+ # Create a compiler and make sure it gets cleaned up
28
+ #
29
+ # @param options [Options]
30
+ # @param runtime [Runtime] The initial +Runtime+ for the compiler
31
+ def self.with_compiler(options:, runtime:, &block)
32
+ if options.verbose
33
+ ::CSVPlusPlus::BenchmarkedCompiler.with_benchmarks(options:, runtime:) do |c|
34
+ block.call(c)
35
+ end
36
+ else
37
+ block.call(new(options:, runtime:))
38
+ end
39
+ ensure
40
+ runtime.cleanup!
41
+ end
42
+
43
+ sig { params(options: ::CSVPlusPlus::Options, runtime: ::CSVPlusPlus::Runtime::Runtime).void }
44
+ # @param options [Options]
45
+ # @param runtime [Runtime]
46
+ def initialize(options:, runtime:)
47
+ @options = options
48
+ @runtime = runtime
49
+
50
+ # TODO: infer a type
51
+ # allow user-supplied key/values to override anything global or from the code section
52
+ @runtime.def_variables(
53
+ options.key_values.transform_values { |v| ::CSVPlusPlus::Entities::String.new(v.to_s) }
54
+ )
55
+ end
56
+
57
+ sig { params(benchmark: ::Benchmark::Report).void }
58
+ # Attach a +Benchmark+ and a place to store timings to the compiler class.
59
+ #
60
+ # @param benchmark [Benchmark] A +Benchmark+ instance
61
+ def benchmark=(benchmark)
62
+ @benchmark = ::T.let(benchmark, ::T.nilable(::Benchmark::Report))
63
+ @timings = ::T.let([], ::T.nilable(::T::Array[::Benchmark::Tms]))
64
+ end
65
+
66
+ sig { returns(::CSVPlusPlus::Template) }
67
+ # Compile a template and return a +::CSVPlusPlus::Template+ instance ready to be written with a +Writer+
68
+ #
69
+ # @return [Template]
70
+ def compile_template
71
+ parse_code_section!
72
+ rows = parse_csv_section!
73
+
74
+ ::CSVPlusPlus::Template.new(rows:, runtime: @runtime).tap do |t|
75
+ t.validate_infinite_expands(@runtime)
76
+ expanding! { t.expand_rows! }
77
+ bind_all_vars! { t.bind_all_vars!(@runtime) }
78
+ resolve_all_cells!(t)
79
+ end
80
+ end
81
+
82
+ sig { params(block: ::T.proc.params(runtime: ::CSVPlusPlus::Runtime::Runtime).void).void }
83
+ # Write the compiled results
84
+ def outputting!(&block)
85
+ @runtime.start_at_csv! { block.call(@runtime) }
86
+ end
87
+
88
+ protected
89
+
90
+ sig { void }
91
+ # Parses the input file and sets variables on +@runtime+ as necessary
92
+ def parse_code_section!
93
+ @runtime.start! do
94
+ # TODO: this flow can probably be refactored, it used to have more needs back when we had to
95
+ # parse and save the code_section
96
+ parsing_code_section do |input|
97
+ csv_section = ::CSVPlusPlus::Parser::CodeSection.new.parse(input, @runtime)
98
+
99
+ # return the csv_section to the caller because they're gonna re-write input with it
100
+ next csv_section
101
+ end
102
+ end
103
+ end
104
+
105
+ sig { returns(::T::Array[::CSVPlusPlus::Row]) }
106
+ # Parse the CSV section and return an array of +Row+s
107
+ #
108
+ # @return [Array<Row>]
109
+ def parse_csv_section!
110
+ @runtime.start_at_csv! do
111
+ @runtime.map_lines(::CSV.new(::T.unsafe(@runtime.input))) do |csv_row|
112
+ parse_row(::T.cast(csv_row, ::T::Array[::String]))
113
+ end
114
+ end
115
+ ensure
116
+ # we're done with the file and everything is in memory
117
+ @runtime.cleanup!
118
+ end
119
+
120
+ sig { params(template: ::CSVPlusPlus::Template).returns(::T::Array[::T::Array[::CSVPlusPlus::Entities::Entity]]) }
121
+ # Iterates through each cell of each row and resolves it's variable and function references.
122
+ #
123
+ # @param template [Template]
124
+ #
125
+ # @return [Array<Entity>]
126
+ def resolve_all_cells!(template)
127
+ @runtime.start_at_csv! do
128
+ @runtime.map_all_cells(template.rows) do |cell|
129
+ cell.ast = @runtime.resolve_cell_value if cell.ast
130
+ end
131
+ end
132
+ end
133
+
134
+ sig { params(block: ::T.proc.void).void }
135
+ # Expanding rows
136
+ def expanding!(&block)
137
+ @runtime.start_at_csv! { block.call }
138
+ end
139
+
140
+ sig { params(block: ::T.proc.void).void }
141
+ # Binding all [[var=]] directives
142
+ def bind_all_vars!(&block)
143
+ @runtime.start_at_csv! { block.call }
144
+ end
145
+
146
+ private
147
+
148
+ sig { params(block: ::T.proc.params(arg0: ::String).returns(::String)).void }
149
+ def parsing_code_section(&block)
150
+ csv_section = block.call(::T.must(::T.must(@runtime.input).read))
151
+ @runtime.rewrite_input!(csv_section)
152
+ end
153
+
154
+ sig { params(csv_row: ::T::Array[::T.nilable(::String)]).returns(::CSVPlusPlus::Row) }
155
+ # Using the current +@runtime+ and the given +csv_row+ parse it into a +Row+ of +Cell+s
156
+ # +csv_row+ should have already been run through a CSV parser and is an array of strings
157
+ #
158
+ # @param csv_row [Array<Array<String>>]
159
+ #
160
+ # @return [Row]
161
+ def parse_row(csv_row)
162
+ row_modifier = ::CSVPlusPlus::Modifier.new(@options, row_level: true)
163
+
164
+ cells = @runtime.map_row(csv_row) { |value, _cell_index| parse_cell(value || '', row_modifier) }
165
+
166
+ ::CSVPlusPlus::Row.new(cells:, index: @runtime.row_index, modifier: row_modifier)
167
+ end
168
+
169
+ sig { params(value: ::String, row_modifier: ::CSVPlusPlus::Modifier::Modifier).returns(::CSVPlusPlus::Cell) }
170
+ def parse_cell(value, row_modifier)
171
+ cell_modifier = ::CSVPlusPlus::Modifier.new(@options)
172
+ parsed_value = ::CSVPlusPlus::Parser::Modifier.new(cell_modifier:, row_modifier:).parse(value, @runtime)
173
+
174
+ ::CSVPlusPlus::Cell.parse(parsed_value, runtime:, modifier: cell_modifier)
175
+ end
176
+ end
177
+ # rubocop:enable Metrics/ClassLength
178
+ end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Entities
6
+ # Some helpful functions that can be mixed into a class to help building ASTs
7
+ module ASTBuilder
8
+ extend ::T::Sig
9
+
10
+ sig do
11
+ params(
12
+ method_name: ::Symbol,
13
+ args: ::T.untyped,
14
+ kwargs: ::T.untyped,
15
+ block: ::T.untyped
16
+ ).returns(::CSVPlusPlus::Entities::Entity)
17
+ end
18
+ # Let the current class have functions which can build a given entity by calling it's type. For example
19
+ # +number(1)+, +variable(:foo)+
20
+ #
21
+ # @param method_name [Symbol] The +method_name+ to respond to
22
+ # @param args [Array] The arguments to create the entity with
23
+ # @param kwargs [Hash] The arguments to create the entity with
24
+ #
25
+ # @return [Entity, #super]
26
+ # rubocop:disable Naming/BlockForwarding
27
+ def method_missing(method_name, *args, **kwargs, &block)
28
+ entity_class_name = method_name.to_s.split('_').map(&:capitalize).join.to_sym
29
+ ::CSVPlusPlus::Entities.const_get(entity_class_name).new(*args, **kwargs, &block)
30
+ rescue ::NameError
31
+ super
32
+ end
33
+ # rubocop:enable Naming/BlockForwarding
34
+
35
+ sig { params(method_name: ::Symbol, _args: ::T.untyped).returns(::T::Boolean) }
36
+ # Let the current class have functions which can build a given entity by calling it's type. For example
37
+ # +number(1)+, +variable(:foo)+
38
+ #
39
+ # @param method_name [Symbol] The +method_name+ to respond to
40
+ # @param _args [::T.Untyped] The arguments to create the entity with
41
+ #
42
+ # @return [::T::Boolean, #super]
43
+ def respond_to_missing?(method_name, *_args)
44
+ !::CSVPlusPlus::Entities::Type.deserialize(method_name.to_s.gsub('_', '')).nil?
45
+ rescue ::KeyError
46
+ super
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Entities
6
+ # A boolean value
7
+ #
8
+ # @attr_reader value [true, false]
9
+ class Boolean < Entity
10
+ sig { returns(::T::Boolean) }
11
+ attr_reader :value
12
+
13
+ sig { params(value: ::T.any(::String, ::T::Boolean)).void }
14
+ # @param value [::String, boolean]
15
+ def initialize(value)
16
+ super(::CSVPlusPlus::Entities::Type::Boolean)
17
+ # TODO: probably can do a lot better in general on type validation
18
+ @value = ::T.let(value.is_a?(::String) ? (value.downcase == 'true') : value, ::T::Boolean)
19
+ end
20
+
21
+ sig { override.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
22
+ # @param _runtime [Runtime]
23
+ #
24
+ # @return [::String]
25
+ def evaluate(_runtime)
26
+ @value.to_s.upcase
27
+ end
28
+
29
+ sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
30
+ # @param other [Entity]
31
+ #
32
+ # @return [::T::Boolean]
33
+ def ==(other)
34
+ return false unless super
35
+
36
+ other.is_a?(self.class) && value == other.value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,58 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Entities
6
+ # Provides ASTs for builtin functions and variables
7
+ module Builtins
8
+ extend ::CSVPlusPlus::Entities::ASTBuilder
9
+
10
+ VARIABLES = {
11
+ # The number (integer) of the current cell. Starts at 1
12
+ cellnum: runtime_value(->(r) { number(r.cell_index + 1) }),
13
+
14
+ # A reference to the current cell
15
+ cellref: runtime_value(->(r) { cell_reference(row_index: r.row_index, cell_index: r.cell_index) }),
16
+
17
+ # A reference to the row above
18
+ rowabove: runtime_value(->(r) { cell_reference(row_index: [0, (r.row_index - 1)].max) }),
19
+
20
+ # A reference to the row below
21
+ rowbelow: runtime_value(->(r) { cell_reference(row_index: r.row_index + 1) }),
22
+
23
+ # The number (integer) of the current row. Starts at 1
24
+ rownum: runtime_value(->(r) { number(r.rownum) }),
25
+
26
+ # A reference to the current row
27
+ rowref: runtime_value(->(r) { cell_reference(row_index: r.row_index) })
28
+ }.freeze
29
+ public_constant :VARIABLES
30
+
31
+ FUNCTIONS = {
32
+ # TODO: A reference to a cell in a given row?
33
+ # A reference to a cell above the current row
34
+ # cellabove: runtime_value(->(r, args) { cell_reference(ref: [args[0], [1, (r.rownum - 1)].max].join) }),
35
+ cellabove: runtime_value(
36
+ lambda { |r, args|
37
+ cell_reference(cell_index: args[0].cell_index, row_index: [0, (r.row_index - 1)].max)
38
+ }
39
+ ),
40
+
41
+ # A reference to a cell in the current row
42
+ celladjacent: runtime_value(
43
+ lambda { |r, args|
44
+ cell_reference(cell_index: args[0].cell_index, row_index: r.row_index)
45
+ }
46
+ ),
47
+
48
+ # A reference to a cell below the current row
49
+ cellbelow: runtime_value(
50
+ lambda { |r, args|
51
+ cell_reference(cell_index: args[0].cell_index, row_index: r.row_index + 1)
52
+ }
53
+ )
54
+ }.freeze
55
+ public_constant :FUNCTIONS
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,231 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Entities
6
+ # A reference to a cell. Internally it is represented by a simple +cell_index+ and +row_index+ but there are
7
+ # functions for converting to and from A1-style formats. Supported formats are:
8
+ #
9
+ # * `1` - A reference to the entire first row
10
+ # * `A` - A reference to the entire first column
11
+ # * `A1` - A reference to the first cell (top left)
12
+ # * `A1:D10` - The range defined between A1 and D10
13
+ # * `Sheet1!B2` - Cell B2 on the sheet "Sheet1"
14
+ #
15
+ # @attr sheet_name [String, nil] The name of the sheet reference
16
+ # @attr_reader cell_index [Integer, nil] The cell index of the cell being referenced
17
+ # @attr_reader row_index [Integer, nil] The row index of the cell being referenced
18
+ # @attr_reader scoped_to_expand [Expand, nil] If set, the expand in which this variable is scoped to. It cannot be
19
+ # resolved outside of the given expand.
20
+ # @attr_reader upper_cell_index [Integer, nil] If set, the cell reference is a range and this is the upper cell
21
+ # index of it
22
+ # @attr_reader upper_row_index [Integer, nil] If set, the cell reference is a range and this is the upper row index
23
+ # of it
24
+ # rubocop:disable Metrics/ClassLength
25
+ class CellReference < Entity
26
+ extend ::T::Sig
27
+
28
+ sig { returns(::T.nilable(::String)) }
29
+ attr_accessor :sheet_name
30
+
31
+ sig { returns(::T.nilable(::Integer)) }
32
+ attr_reader :cell_index
33
+
34
+ sig { returns(::T.nilable(::Integer)) }
35
+ attr_reader :row_index
36
+
37
+ sig { returns(::T.nilable(::CSVPlusPlus::Modifier::Expand)) }
38
+ attr_reader :scoped_to_expand
39
+
40
+ sig { returns(::T.nilable(::Integer)) }
41
+ attr_reader :upper_cell_index
42
+
43
+ sig { returns(::T.nilable(::Integer)) }
44
+ attr_reader :upper_row_index
45
+
46
+ # TODO: this is getting gross, maybe define an actual parser
47
+ A1_NOTATION_REGEXP = /
48
+ ^
49
+ (?:
50
+ (?:
51
+ (?:'([^'\\]|\\.)*') # allow for a single-quoted sheet name
52
+ |
53
+ (\w+) # or if it's not quoted, just allow \w+
54
+ )
55
+ ! # if a sheet name is specified, it's always followed by a !
56
+ )?
57
+ ([a-zA-Z0-9]+) # the only part required - something alphanumeric
58
+ (?: :([a-zA-Z0-9]+))? # and they might make it a range
59
+ $
60
+ /x
61
+ public_constant :A1_NOTATION_REGEXP
62
+
63
+ ALPHA = ::T.let(('A'..'Z').to_a.freeze, ::T::Array[::String])
64
+ private_constant :ALPHA
65
+
66
+ sig { params(cell_reference_string: ::String).returns(::T::Boolean) }
67
+ # Does the given +cell_reference_string+ conform to a valid cell reference?
68
+ #
69
+ # {https://developers.google.com/sheets/api/guides/concepts}
70
+ #
71
+ # @param cell_reference_string [::String] The string to check if it is a valid cell reference (we assume it's in
72
+ # A1 notation but maybe can support R1C1)
73
+ #
74
+ # @return [::T::Boolean]
75
+ def self.valid_cell_reference?(cell_reference_string)
76
+ !(cell_reference_string =~ ::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP).nil?
77
+ end
78
+
79
+ sig do
80
+ params(
81
+ cell_index: ::T.nilable(::Integer),
82
+ ref: ::T.nilable(::String),
83
+ row_index: ::T.nilable(::Integer),
84
+ scoped_to_expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)
85
+ ).void
86
+ end
87
+ # Either +ref+, +cell_index+ or +row_index+ must be specified.
88
+ #
89
+ # @param cell_index [Integer, nil] The index of the cell being referenced.
90
+ # @param ref [Integer, nil] An A1-style cell reference (that will be parsed into it's row/cell indexes).
91
+ # @param row_index [Integer, nil] The index of the row being referenced.
92
+ # @param scoped_to_expand [Expand] The [[expand]] that this cell reference will be scoped to. In other words, it
93
+ # will only be able to be resolved if the runtime is within the bounds of the expand (it can't be referenced
94
+ # outside of the expand.)
95
+ # rubocop:disable Metrics/MethodLength
96
+ def initialize(cell_index: nil, ref: nil, row_index: nil, scoped_to_expand: nil)
97
+ raise(::ArgumentError, 'Must specify :ref, :cell_index or :row_index') unless ref || cell_index || row_index
98
+
99
+ super(::CSVPlusPlus::Entities::Type::CellReference)
100
+
101
+ if ref
102
+ from_a1_ref!(ref)
103
+ else
104
+ @cell_index = ::T.let(cell_index, ::T.nilable(::Integer))
105
+ @row_index = ::T.let(row_index, ::T.nilable(::Integer))
106
+
107
+ @upper_cell_index = ::T.let(nil, ::T.nilable(::Integer))
108
+ @upper_row_index = ::T.let(nil, ::T.nilable(::Integer))
109
+ end
110
+
111
+ @scoped_to_expand = scoped_to_expand
112
+ end
113
+ # rubocop:enable Metrics/MethodLength
114
+
115
+ sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
116
+ # @param other [Entity]
117
+ #
118
+ # @return [boolean]
119
+ # rubocop:disable Metrics/CyclomaticComplexity
120
+ def ==(other)
121
+ return false unless super
122
+
123
+ other.is_a?(self.class) && @cell_index == other.cell_index && @row_index == other.row_index \
124
+ && @sheet_name == other.sheet_name && @scoped_to_expand == other.scoped_to_expand \
125
+ && @upper_cell_index == other.upper_cell_index && @upper_row_index == other.upper_row_index
126
+ end
127
+ # rubocop:enable Metrics/CyclomaticComplexity
128
+
129
+ sig { override.params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
130
+ # Get the A1-style cell reference
131
+ #
132
+ # @param runtime [Runtime] The current runtime
133
+ #
134
+ # @return [::String] An A1-style reference
135
+ def evaluate(runtime)
136
+ # unless in_scope?(runtime)
137
+ # runtime.raise_modifier_syntax_error(message: 'Reference is out of scope', bad_input: runtime.cell.value)
138
+ # end
139
+
140
+ to_a1_ref(runtime) || ''
141
+ end
142
+
143
+ sig { returns(::T::Boolean) }
144
+ # Is the cell_reference a range? - something like A1:D10
145
+ #
146
+ # @return [boolean]
147
+ def range?
148
+ !upper_row_index.nil? || !upper_cell_index.nil?
149
+ end
150
+
151
+ private
152
+
153
+ sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T.nilable(::String)) }
154
+ # Turns index-based/X,Y coordinates into a A1 format
155
+ #
156
+ # @param runtime [Runtime]
157
+ #
158
+ # @return [::String, nil]
159
+ def to_a1_ref(runtime)
160
+ row_index = runtime_row_index(runtime)
161
+ return unless row_index || @cell_index
162
+
163
+ rowref = row_index ? (row_index + 1).to_s : ''
164
+ cellref = @cell_index ? to_a1_cell_ref : ''
165
+ [cellref, rowref].join
166
+ end
167
+
168
+ sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T.nilable(::Integer)) }
169
+ def runtime_row_index(runtime)
170
+ @scoped_to_expand ? runtime.row_index : @row_index
171
+ end
172
+
173
+ sig { returns(::String) }
174
+ # Turns a cell index into an A1 reference (just the "A" part - for example 0 == 'A', 1 == 'B', 2 == 'C', etc.)
175
+ #
176
+ # @return [::String]
177
+ def to_a1_cell_ref
178
+ c = @cell_index.dup
179
+ ref = ''
180
+
181
+ while c >= 0
182
+ # rubocop:disable Lint/ConstantResolution
183
+ ref += ::T.must(ALPHA[c % 26])
184
+ # rubocop:enable Lint/ConstantResolution
185
+ c = (c / 26).floor - 1
186
+ end
187
+
188
+ ref.reverse
189
+ end
190
+
191
+ sig { params(ref: ::String).void }
192
+ def from_a1_ref!(ref)
193
+ quoted_sheet_name, unquoted_sheet_name, lower_range, upper_range = ::T.must(
194
+ ref.strip.match(
195
+ ::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP
196
+ )
197
+ ).captures
198
+
199
+ @sheet_name = quoted_sheet_name || unquoted_sheet_name
200
+
201
+ parse_lower_range!(lower_range) if lower_range
202
+ parse_upper_range!(upper_range) if upper_range
203
+ end
204
+
205
+ sig { params(lower_range: ::String).void }
206
+ def parse_lower_range!(lower_range)
207
+ cell_ref, row_ref = ::T.must(lower_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
208
+ @cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
209
+ @row_index = Integer(row_ref, 10) - 1 if row_ref
210
+ end
211
+
212
+ sig { params(upper_range: ::String).void }
213
+ # TODO: make this less redundant with the above function
214
+ def parse_upper_range!(upper_range)
215
+ cell_ref, row_ref = ::T.must(upper_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
216
+ @upper_cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
217
+ @upper_row_index = Integer(row_ref, 10) - 1 if row_ref
218
+ end
219
+
220
+ sig { params(cell_ref: ::String).returns(::Integer) }
221
+ def from_a1_cell_ref!(cell_ref)
222
+ (cell_ref.upcase.chars.reduce(0) do |cell_index, letter|
223
+ # rubocop:disable Lint/ConstantResolution
224
+ (cell_index * 26) + ::T.must(ALPHA.find_index(letter)) + 1
225
+ # rubocop:enable Lint/ConstantResolution
226
+ end) - 1
227
+ end
228
+ end
229
+ # rubocop:enable Metrics/ClassLength
230
+ end
231
+ end
@@ -0,0 +1,63 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Entities
6
+ # A date value
7
+ #
8
+ # @attr_reader value [Date] The parsed date
9
+ class Date < Entity
10
+ extend ::T::Sig
11
+
12
+ sig { returns(::Date) }
13
+ attr_reader :value
14
+
15
+ # TODO: support time granularity?
16
+ DATE_STRING_REGEXP = %r{^\d{1,2}[/-]\d{1,2}[/-]\d{1,4}?$}
17
+ public_constant :DATE_STRING_REGEXP
18
+
19
+ sig { params(date_string: ::String).returns(::T::Boolean) }
20
+ # Is the given string a valid date?
21
+ #
22
+ # @param date_string [::String]
23
+ def self.valid_date?(date_string)
24
+ new(date_string)
25
+ true
26
+ rescue ::Date::Error
27
+ false
28
+ end
29
+
30
+ sig { params(value: ::String).void }
31
+ # @param value [::String] The user-inputted date value
32
+ def initialize(value)
33
+ super(::CSVPlusPlus::Entities::Type::Date)
34
+
35
+ parsed =
36
+ begin
37
+ ::Date.parse(value)
38
+ rescue ::Date::Error
39
+ ::Date.strptime(value, '%d/%m/%yyyy')
40
+ end
41
+ @value = ::T.let(parsed, ::Date)
42
+ end
43
+
44
+ sig { override.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
45
+ # @param _runtime [Runtime]
46
+ #
47
+ # @return [::String]
48
+ def evaluate(_runtime)
49
+ @value.strftime('%m/%d/%y')
50
+ end
51
+
52
+ sig { override.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
53
+ # @param other [Entity]
54
+ #
55
+ # @return [T::Boolean]
56
+ def ==(other)
57
+ return false unless super
58
+
59
+ other.is_a?(self.class) && other.value == @value
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Entities
6
+ # A basic building block of the abstract syntax tree (AST)
7
+ #
8
+ # @attr_reader id [Symbol] The identifier of the entity. For functions this is the function name,
9
+ # for variables it's the variable name
10
+ # @attr_reader type [Entities::Type] The type of the entity. Each type should have a corresponding class definition
11
+ # in CSVPlusPlus::Entities
12
+ class Entity
13
+ extend ::T::Sig
14
+ extend ::T::Helpers
15
+
16
+ abstract!
17
+
18
+ sig { returns(::T.nilable(::Symbol)) }
19
+ attr_reader :id
20
+
21
+ sig { returns(::CSVPlusPlus::Entities::Type) }
22
+ attr_reader :type
23
+
24
+ sig { params(type: ::CSVPlusPlus::Entities::Type, id: ::T.nilable(::Symbol)).void }
25
+ # @param type [Entities::Type]
26
+ # @param id [Symbol, nil]
27
+ def initialize(type, id: nil)
28
+ @type = type
29
+ @id = ::T.let(id&.downcase&.to_sym || nil, ::T.nilable(::Symbol))
30
+ end
31
+
32
+ sig { overridable.params(other: ::CSVPlusPlus::Entities::Entity).returns(::T::Boolean) }
33
+ # Each class should define it's own version of #==
34
+ # @param other [Entity]
35
+ #
36
+ # @return [boolean]
37
+ def ==(other)
38
+ self.class == other.class && @type == other.type && @id == other.id
39
+ end
40
+
41
+ sig { abstract.params(_runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::String) }
42
+ # Uses the given +runtime+ to evaluate itself in the current context
43
+ #
44
+ # @param _runtime [Runtime] The current runtime
45
+ #
46
+ # @return [::String]
47
+ def evaluate(_runtime); end
48
+ end
49
+ end
50
+ end