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